Commit 84c3d7de authored by Benni Mack's avatar Benni Mack
Browse files

[TASK] Move extension upload out of SOAP API

The actual logic for uploading a new TER extension version
is moved into the ExtensionVersion->upload() method,
which handles all checks.

This way, the tx_ter_api is not needed anymore outside
the SOAP API (except for TerService, which will be adapted
separately to work without tx_ter_api).
parent f4f4ed16
Pipeline #9307 passed with stages
in 9 minutes and 20 seconds
......@@ -13,9 +13,12 @@ namespace T3o\Ter\Api;
use T3o\Ter\Exception\FailedDependencyException;
use T3o\Ter\Exception\InternalServerErrorException;
use T3o\Ter\Exception\InvalidExtensionVersionFormatException;
use T3o\Ter\Exception\NotFoundException;
use T3o\Ter\Exception\NoUploadCommentException;
use T3o\Ter\Exception\UnauthorizedException;
use T3o\TerFe2\Service\LTSVersionService;
use T3o\TerFe2\Utility\ArchiveUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -38,6 +41,29 @@ class ExtensionVersion
*/
protected $version;
/**
* 30MB Maximum upload size for extensions
*
* @var int
*/
protected const EXTENSION_MAX_UPLOAD_SIZE = 31457280;
// Create a list of allowed images; resulting file names are given
// as values (the base path is prefixed then)
protected const POTENTIAL_IMAGE_PATHS = [
// Extension icon (either SVG, PNG or GIF)
'ext_icon.svg' => '.svg',
'ext_icon.png' => '.png',
'ext_icon.gif' => '.gif',
'Resources/Public/Icons/Extension.svg' => '.svg',
'Resources/Public/Icons/Extension.png' => '.png',
'Resources/Public/Icons/Extension.gif' => '.gif',
// Small preview image 220x150
'Resources/Public/Images/Distribution.png' => '_Distribution.png',
// Big welcome image 300x400
'Resources/Public/Images/DistributionWelcome.png' => '_DistributionWelcome.png'
];
public function __construct(ExtensionKey $extensionKey, string $version)
{
$this->extensionKey = $extensionKey;
......@@ -244,4 +270,340 @@ class ExtensionVersion
}
}
}
/**
* Creates a new version of an extension (or replaces an existing version is possible if the uploader is an admin).
*
* Effectively creates all information for the t3x file, creates the file and the preview images,
* afterwards stores everything in the database in tx_ter_extensions.
*
* @param ApiUser $uploader
* @param object $extensionInfoData
* @param object[] $files
* @throws FailedDependencyException
* @throws InternalServerErrorException
* @throws NotFoundException
* @throws UnauthorizedException
* @throws NoUploadCommentException
*/
public function upload(ApiUser $uploader, object $extensionInfoData, array $files): void
{
if (!$this->isValidVersionNumber()) {
throw new InvalidExtensionVersionFormatException(
'Your version number "' . htmlspecialchars($this->getVersion()) . '" is invalid. Allowed are three numbers with maximum 999, e.g. "7.8.999".',
1429912029
);
}
if (!$this->extensionKey->isRegistered()) {
throw new NotFoundException('Extension does not exist.', ResultCodes::ERROR_UPLOADEXTENSION_EXTENSIONDOESNTEXIST);
}
if (!$uploader->isAuthenticated() || !$this->extensionKey->isApiUserOwnerOrAdmin($uploader)) {
throw new UnauthorizedException('Access denied.', ResultCodes::ERROR_UPLOADEXTENSION_ACCESSDENIED);
}
if (!$uploader->isAdministrator()) {
try {
$this->checkExtensionDependencyOnSupportedTypo3Version($extensionInfoData->technicalData->dependencies);
} catch (FailedDependencyException $e) {
switch ($e->getCode()) {
case ResultCodes::ERROR_UPLOADEXTENSION_TYPO3DEPENDENCYINCORRECT:
$message = 'Extension does not have a dependency for a supported version of TYPO3. See http://typo3.org/news/article/announcing-ter-cleanup-process/ for how to fix this.';
break;
default:
$message = 'Check on dependency for a supported version of TYPO3 failed due to technical reasons';
break;
}
throw new FailedDependencyException($message, $e->getCode());
}
}
if (trim((string)$extensionInfoData->infoData->uploadComment) === '') {
throw new NoUploadCommentException(
'You need to set an upload comment!',
ResultCodes::ERROR_UPLOADEXTENSION_NOUPLOADCOMMENT
);
}
$t3xCheckSum = $this->uploadWriteExtensionFiles($extensionInfoData, $files);
$this->writeExtensionVersionInformationToDatabase($uploader->getUsername(), $extensionInfoData, $files, $t3xCheckSum);
}
/**
* Creates a T3X file by using the extension info data and the files data and
* writes the file to the repository's directory. By default this function creates
* gzip compressed T3X files.
*
* After writing the extension, some files specific data (like the file size) is
* added to $extensionInfoData for later informational use.
*
* This function additionally extracts the preview images and the extension icon by calling
* saveImages().
*
* @param object $extensionInfoData The general extension information as received by the SOAP server
* @param object[] $files The array of file data objects as received by the SOAP server
* @return string the md5 hash sum of the written t3x file (stored in the database then)
*
* @throws InternalServerErrorException
* @throws NotFoundException
*/
protected function uploadWriteExtensionFiles(object $extensionInfoData, array $files): string
{
$preparedFilesDataArr = [];
foreach ($files as $fileData) {
$decodedContent = base64_decode($fileData->content);
if ($fileData->contentMD5 != md5($decodedContent)) {
throw new NotFoundException(
'MD5 does not match for file ' . (string)$fileData->name,
ResultCodes::ERROR_UPLOADEXTENSION_FILEMD5DOESNOTMATCH
);
}
$preparedFilesDataArr[$fileData->name] = [
'name' => $fileData->name,
'size' => $fileData->size,
'mtime' => $fileData->modificationTime,
'is_executable' => $fileData->isExecutable,
'content' => $decodedContent,
'content_md5' => md5($decodedContent)
];
}
$constraints = [];
if (is_array($extensionInfoData->technicalData->dependencies)) {
foreach ($extensionInfoData->technicalData->dependencies as $dependency) {
$dependency = json_decode(json_encode($dependency), true);
if (!empty($dependency['kind']) && !empty($dependency['extensionKey']) && in_array(
$dependency['kind'],
['depends', 'conflicts', 'suggests'],
true
)
) {
$constraints[$dependency['kind']][$dependency['extensionKey']] = (string)$dependency['versionRange'];
}
}
}
// Prepare EM_CONF Array:
$preparedEMConfArr = [
'title' => $extensionInfoData->metaData->title,
'description' => $extensionInfoData->metaData->description,
'category' => $extensionInfoData->metaData->category,
'version' => $this->getVersion(),
'state' => $extensionInfoData->metaData->state,
'uploadfolder' => in_array(strtolower((string)$extensionInfoData->technicalData->uploadFolder), ['1', 'false'], true),
'createDirs' => $extensionInfoData->technicalData->createDirs,
'clearcacheonload' => in_array(strtolower((string)$extensionInfoData->technicalData->clearCacheOnLoad), ['1', 'false'], true),
'author' => $extensionInfoData->metaData->authorName,
'author_email' => $extensionInfoData->metaData->authorEmail,
'author_company' => $extensionInfoData->metaData->authorCompany,
'constraints' => $constraints
];
// Compile T3X Data Array:
$dataArr = [
'extKey' => strtolower((string)$this->getExtensionKey()),
'EM_CONF' => $preparedEMConfArr,
'misc' => [],
'techInfo' => [],
'FILES' => $preparedFilesDataArr
];
$t3xFileUncompressedData = serialize($dataArr);
$t3xFileData = md5($t3xFileUncompressedData) . ':gzcompress:' . gzcompress($t3xFileUncompressedData);
$this->writeFiles($t3xFileData, $preparedFilesDataArr);
return md5($t3xFileData);
}
/**
* Extracts information from the extension information and stores it as a record in the database
* tables. These tables are used for quickly getting information about extensions for display
* in the TER frontend plugin.
*
* Note: All data written to the database (like everywhere else in the TER handling) is assumed to be
* utf-8 encoded!
*
* @param string $uploaderUserName
* @param object $extensionInfoData : The general extension information as received by the SOAP server
* @param object[] $files The array of file data objects as received by the SOAP server
* @param string $t3xCheckSum a md5 hash sum of the created t3x file
*/
protected function writeExtensionVersionInformationToDatabase($uploaderUserName, $extensionInfoData, array $files, string $t3xCheckSum): void
{
// Prepare information about files
$extensionInfoData->technicalData->isManualIncluded = 0;
$composerInfo = [];
foreach ($files as $fileData) {
$extensionInfoData->infoData->files[$fileData->name] = [
'filename' => $fileData->name,
'size' => $fileData->size,
'mtime' => $fileData->modificationTime,
'is_executable' => $fileData->isExecutable,
];
switch (trim($fileData->name, '/\\')) {
case 'doc/manual.sxw':
$extensionInfoData->technicalData->isManualIncluded = 1;
break;
case 'ext_emconf.php':
$autoload = [];
if (is_callable(ArchiveUtility::class . '::extractEmConf')) {
$emConfData = ArchiveUtility::extractEmConf(base64_decode($fileData->content));
if (!empty($emConfData['autoload'])) {
$autoload = $emConfData['autoload'];
}
}
break;
case 'composer.json':
$composerJsonData = json_decode(base64_decode($fileData->content), true);
if (!empty($composerJsonData['autoload'])) {
$composerInfo['autoload'] = $composerJsonData['autoload'];
}
$composerInfo['name'] = $composerJsonData['name'];
break;
default:
// Nothing to do here
}
}
if (empty($composerInfo) && !empty($autoload)) {
$composerInfo = ['autoload' => $autoload];
}
$extensionInfoData->technicalData->composerInfo = $composerInfo;
$extensionKey = strtolower((string)$this->getExtensionKey());
// Get dependencies
$dependenciesArr = [];
if (is_array($extensionInfoData->technicalData->dependencies)) {
foreach ($extensionInfoData->technicalData->dependencies as $dependency) {
$dependencyAsArray = (array)$dependency;
if ($dependencyAsArray['extensionKey']) {
$dependenciesArr[] = [
'kind' => $dependencyAsArray['kind'],
'extensionKey' => $dependencyAsArray['extensionKey'],
'versionRange' => $dependencyAsArray['versionRange']
];
}
}
}
// Cleanup some attributes
$attributes = ['uploadFolder', 'clearCacheOnLoad'];
foreach ($attributes as $attribute) {
if (empty($extensionInfoData->technicalData->$attribute) || $extensionInfoData->technicalData->$attribute === 'false') {
$extensionInfoData->technicalData->$attribute = false;
} else {
$extensionInfoData->technicalData->$attribute = true;
}
}
// Prepare the new records
$apiConfiguration = GeneralUtility::makeInstance(Configuration::class);
$extensionRow = [
'tstamp' => $GLOBALS['SIM_EXEC_TIME'],
'crdate' => $GLOBALS['SIM_EXEC_TIME'],
'pid' => $apiConfiguration->getStoragePid(),
'extensionkey' => $extensionKey,
'version' => $this->getVersion(),
'title' => $extensionInfoData->metaData->title,
'description' => $extensionInfoData->metaData->description,
'state' => $extensionInfoData->metaData->state,
'category' => $extensionInfoData->metaData->category,
'ismanualincluded' => $extensionInfoData->technicalData->isManualIncluded,
't3xfilemd5' => $t3xCheckSum,
'reviewstate' => 0,
'uploadcomment' => (string)$extensionInfoData->infoData->uploadComment,
'lastuploadbyusername' => $uploaderUserName,
'lastuploaddate' => $GLOBALS['SIM_EXEC_TIME'],
'files' => serialize($extensionInfoData->infoData->files),
'techinfo' => serialize($extensionInfoData->infoData->techInfo),
'composerinfo' => json_encode($extensionInfoData->technicalData->composerInfo),
'dependencies' => serialize($dependenciesArr),
'createdirs' => (string)$extensionInfoData->technicalData->createDirs,
'uploadfolder' => (int)$extensionInfoData->technicalData->uploadFolder,
'clearcacheonload' => (int)$extensionInfoData->technicalData->clearCacheOnLoad,
'authorname' => (string)$extensionInfoData->metaData->authorName,
'authoremail' => (string)$extensionInfoData->metaData->authorEmail,
'authorcompany' => (string)$extensionInfoData->metaData->authorCompany,
];
// Update an existing or insert a new extension record
$uidOfExtensionVersion = $this->getUidOfExtensionVersion();
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_ter_extensions');
if ($uidOfExtensionVersion > 0) {
$conn->update('tx_ter_extensions', $extensionRow, ['uid' => $uidOfExtensionVersion]);
} else {
$conn->insert('tx_ter_extensions', $extensionRow);
}
// Put new extension version into queue
GeneralUtility::makeInstance(UploadQueue::class)->addExtensionVersionToQueue($this);
}
/**
* Writes the t3x image to disk, and also the prepared image files.
*
* @param string $t3xFileData
* @param array $preparedFilesDataArr
* @throws InternalServerErrorException
* @throws NotFoundException
*/
protected function writeFiles(string $t3xFileData, array $preparedFilesDataArr): void
{
// Check if size of t3x file is too big
if (strlen($t3xFileData) > self::EXTENSION_MAX_UPLOAD_SIZE) {
throw new NotFoundException(
'The extension size exceeded ' . self::EXTENSION_MAX_UPLOAD_SIZE . ' bytes which is the maximum for extension uploads.',
ResultCodes::ERROR_UPLOADEXTENSION_EXTENSIONTOOBIG
);
}
// Create directories and build filenames
$storagePrefix = $this->getStoragePrefix();
GeneralUtility::mkdir_deep(dirname($storagePrefix));
$t3xFileName = $storagePrefix . '.t3x';
// Write the t3x file
$fh = @fopen($t3xFileName, 'wb');
if (!$fh) {
throw new InternalServerErrorException(
'Write error while writing .t3x file: ' . $t3xFileName,
ResultCodes::ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGFILES
);
}
fwrite($fh, $t3xFileData);
fclose($fh);
$this->saveImages($preparedFilesDataArr, $storagePrefix);
}
/**
* Saves the images that are displayed in the TER listing and detail views
*
* @param array $preparedFilesDataArr The array with file names and contents
* @param string $storagePrefix The prefix for each image with the full path, is also the name for the ext_icon file
*
* @throws InternalServerErrorException
*/
protected function saveImages(array $preparedFilesDataArr, string $storagePrefix): void
{
$foundImages = array_intersect_key($preparedFilesDataArr, self::POTENTIAL_IMAGE_PATHS);
foreach ($foundImages as $image) {
$imageName = self::POTENTIAL_IMAGE_PATHS[$image['name']];
$imageData = $image['content'];
$imagePath = $storagePrefix . $imageName;
if (strlen($imageData)) {
$fh = @fopen($imagePath, 'wb');
if (!$fh) {
throw new InternalServerErrorException(
'Write error while writing file: ' . $imagePath,
ResultCodes::ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGFILES
);
}
fwrite($fh, $imageData);
fclose($fh);
}
}
}
}
<?php
declare(strict_types = 1);
namespace T3o\Ter\Exception;
/*
* 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!
*/
class InvalidExtensionVersionFormatException extends Exception
{
}
This diff is collapsed.
......@@ -14,7 +14,9 @@ namespace T3o\TerFe2\Controller;
* The TYPO3 project - inspiring people to share!
*/
use T3o\Ter\Api\ApiUser;
use T3o\Ter\Api\ExtensionKey;
use T3o\Ter\Api\ExtensionVersion;
use T3o\TerFe2\Validation\Validator\ComposerNameValidator;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Page\PageRenderer;
......@@ -509,9 +511,14 @@ class ExtensionController extends \T3o\TerFe2\Controller\AbstractController
}
$extensionInfo->extensionKey = $extensionKey;
$extensionInfo->infoData->uploadComment = $form['comment'];
$filesData = (object)['fileData' => $files];
try {
GeneralUtility::makeInstance(\tx_ter_api::class)->uploadExtensionWithoutSoap($this->frontendUser['username'], $extensionInfo, $filesData);
// Use the API to upload the file as t3x file
// This code will be reworked into a full-blown API
$uploader = new ApiUser($this->frontendUser['username']);
$uploader->authenticate();
$extensionKeyObject = new ExtensionKey($extensionKey);
$extensionVersion = new ExtensionVersion($extensionKeyObject, $extensionInfo->version);
$extensionVersion->upload($uploader, $extensionInfo, $files);
$this->redirect('index', 'Registerkey', null, ['uploaded' => true], $this->settings['pages']['manageKeysPID']);
} catch (\Exception $exception) {
$this->forwardWithError('Error ' . $exception->getCode() . ': ' . $exception->getMessage(), 'uploadVersion');
......
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