Commit 450ec6ae authored by Benni Mack's avatar Benni Mack
Browse files

[FEATURE] Encapsulate "tx_ter_extensionkeys" access

A new object "ExtensionKey" now allows to work with Extension Keys
in the SOAP API with an object-oriented approach.

This heavily reduces the tx_ter_helper class and removes helper
functionality from tx_ter_api.

The new ApiUser is now used to validate requests that need a valid
user (registering an API key, or transferring ownership) as argument.
parent d3960931
Pipeline #9115 passed with stages
in 6 minutes and 31 seconds
<?php
declare(strict_types = 1);
namespace T3o\Ter\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", 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\Ter\Exception\ExtensionKeyAlreadyInUseException;
use T3o\Ter\Exception\ExtensionKeyNotFoundException;
use T3o\Ter\Exception\InternalServerErrorException;
use T3o\Ter\Exception\InvalidExtensionKeyFormatException;
use T3o\Ter\Exception\UnauthorizedException;
use T3o\Ter\Exception\UserNotFoundException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class dealing with Extension Keys.
*
* Checks if an extension key is registered, or has a valid syntax, returns the Owner.
*
* With a proper ApiUser, keys can also be registered or ownership can be transferred.
*/
class ExtensionKey
{
/**
* @var string
*/
protected $extensionKey;
public function __construct(string $extensionKey)
{
$this->extensionKey = $extensionKey;
}
public function __toString()
{
return $this->extensionKey;
}
public function isValidExtensionKeyName(): bool
{
return $this->validate($this->extensionKey);
}
/**
* Checks if the given extension key is unique and registered.
* Takes underscores into account, so the key "ter_ter" can't be registered
* if "te_rt_er" or "terter" already exist.
*
* @return bool
*/
public function isRegistered(): bool
{
$cleanedExtensionKey = str_replace('_', '', $this->extensionKey);
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_ter_extensionkeys');
$result = $queryBuilder->select('extensionkey')
->from('tx_ter_extensionkeys')
->add(
'where',
'REPLACE(extensionkey, "_", "") = ' . $queryBuilder->createNamedParameter($cleanedExtensionKey)
)
->execute()
->rowCount();
return $result > 0;
}
public function getOwner(): ?string
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_ter_extensionkeys');
$ownerUserName = $queryBuilder
->select('ownerusername')
->from('tx_ter_extensionkeys')
->where(
$queryBuilder->expr()->eq('extensionkey', $queryBuilder->createNamedParameter($this->extensionKey))
)
->execute()
->fetchColumn();
return !empty($ownerUserName) ? $ownerUserName : null;
}
/**
* Useful to see if the user is allowed to do any work on an extension key (transferring, deleting an extension version or key)
*
* @param ApiUser $authenticatedUser
* @return bool
*/
public function isApiUserOwnerOrAdmin(ApiUser $authenticatedUser): bool
{
if (!$authenticatedUser->isAuthenticated()) {
return false;
}
if ($authenticatedUser->isAdministrator()) {
return true;
}
$ownerUserName = $this->getOwner();
if ($ownerUserName === null) {
return false;
}
if (strtolower($ownerUserName) === strtolower($authenticatedUser->getUsername())) {
return true;
}
return false;
}
public function registerKey(
ApiUser $authenticatedUser,
string $title,
string $description,
string $ownerUserName
) {
if (!$authenticatedUser->isAuthenticated()) {
throw new UnauthorizedException('Access denied.', ResultCodes::ERROR_GENERAL_USERNOTFOUND);
}
// User must be admin or the same user as given
if (!$authenticatedUser->isAdministrator() && strtolower($authenticatedUser->getUsername()) !== strtolower($ownerUserName)) {
throw new UnauthorizedException('Access denied.', ResultCodes::ERROR_GENERAL_USERNOTFOUND);
}
if (!$this->isValidExtensionKeyName()) {
throw new InvalidExtensionKeyFormatException('You are trying to register an extension which is already in use');
}
if ($this->isRegistered()) {
throw new ExtensionKeyAlreadyInUseException('You are trying to register an extension which is already in use');
}
$ownerUserName = $this->verifyUsername($ownerUserName);
if (empty($ownerUserName)) {
throw new UserNotFoundException('The requested user does not exist, so it cannot be used as owner');
}
$extensionKeysRow = [
'pid' => GeneralUtility::makeInstance(Configuration::class)->getStoragePid(),
'tstamp' => $GLOBALS['EXEC_TIME'],
'crdate' => $GLOBALS['EXEC_TIME'],
'extensionkey' => $this->extensionKey,
'title' => mb_strcut($title, 0, 50, 'utf-8'),
'description' => mb_strcut($description, 0, 255, 'utf-8'),
'ownerusername' => $ownerUserName
];
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_ter_extensionkeys');
$result = $conn->insert('tx_ter_extensionkeys', $extensionKeysRow);
if ($result === 0) {
throw new InternalServerErrorException(
'Database error while inserting extension key.',
ResultCodes::ERROR_REGISTEREXTENSIONKEY_DBERRORWHILEINSERTINGKEY
);
}
return true;
}
public function transferOwnership(ApiUser $authenticatedUser, string $newOwnerName): void
{
if (!$authenticatedUser->isAuthenticated()) {
throw new UnauthorizedException('Access denied.', ResultCodes::ERROR_MODIFYEXTENSIONKEY_ACCESSDENIED);
}
if (!$this->isRegistered()) {
throw new ExtensionKeyNotFoundException('The user is not an active typo3.org user', ResultCodes::ERROR_MODIFYEXTENSIONKEY_KEYDOESNOTEXIST);
}
// Only admins and the current extension owner can transfer ownership
if (!$this->isApiUserOwnerOrAdmin($authenticatedUser)) {
throw new UnauthorizedException('Access denied.', ResultCodes::ERROR_MODIFYEXTENSIONKEY_ACCESSDENIED);
}
// Check if the new user is valid
$newOwnerName = $this->verifyUsername($newOwnerName);
if (empty($newOwnerName)) {
throw new UserNotFoundException('The user is not an active typo3.org user', ResultCodes::ERROR_GENERAL_USERNOTFOUND);
}
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_ter_extensionkeys');
$affectedRows = $conn->update(
'tx_ter_extensionkeys',
['ownerusername' => $newOwnerName],
['extensionkey' => $this->extensionKey]
);
if (!$affectedRows) {
throw new InternalServerErrorException(
'Database error while updating extension key.',
ResultCodes::ERROR_GENERAL_DATABASEERROR
);
}
}
/**
* Check if a user with such a name exists, and return the username from the DB (= with proper lowercase etc)
* or null, if the user does not exist.
*
* @param string $username
* @return string|null
*/
protected function verifyUsername(string $username): ?string
{
// Check if the new user is valid
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_users');
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
$username = $queryBuilder
->select('username')
->from('fe_users')
->where(
$queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($username))
)
->execute()
->fetchColumn();
return !empty($username) ? $username : null;
}
/**
* Checks if an extension key is formally valid
*
* @param string $extensionKey
* @return bool
*/
protected function validate(string $extensionKey): bool
{
// check for forbidden characters
if (preg_match('/[^a-z0-9_]/', $extensionKey)) {
return false;
}
// check for forbidden start and end characters
if (preg_match('/^[0-9_]/', $extensionKey) || preg_match('/[_]$/', $extensionKey)) {
return false;
}
// Length
$extensionKeyLength = strlen($extensionKey);
if ($extensionKeyLength < 3 || $extensionKeyLength > 30) {
return false;
}
// Bad prefixes:
$badPrefixesArr = ['tx', 'user_', 'pages', 'tt_', 'sys_', 'ts_language_', 'csh_'];
foreach ($badPrefixesArr as $prefix) {
if (GeneralUtility::isFirstPartOfStr($extensionKey, $prefix)) {
return false;
}
}
return true;
}
/**
* @todo: this method does not belong here, should be moved to a different location and return objects in the future
*
* @param string|null $limitToUserName
* @param string|null $limitToTitle
* @param string|null $limitToDescription
* @param string|null $limitToExtensionKey
* @return array|null
*/
public static function findExtensionKeys(
?string $limitToUserName,
?string $limitToTitle,
?string $limitToDescription,
?string $limitToExtensionKey
): ?array {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_ter_extensionkeys');
$queryBuilder
->select('extensionkey,title,description,ownerusername')
->from('tx_ter_extensionkeys');
if ($limitToUserName) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('ownerusername', $queryBuilder->createNamedParameter($limitToUserName))
);
}
if ($limitToTitle) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($limitToTitle))
);
}
if ($limitToDescription) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('description', $queryBuilder->createNamedParameter($limitToDescription))
);
}
if ($limitToExtensionKey) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('extensionkey', $queryBuilder->createNamedParameter($limitToExtensionKey))
);
}
$extensionKeys = $queryBuilder->execute()->fetchAll();
if (!empty($extensionKeys)) {
return $extensionKeys;
}
return null;
}
}
<?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 ExtensionKeyAlreadyInUseException extends Exception
{
}
<?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 ExtensionKeyNotFoundException extends Exception
{
}
<?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 InvalidExtensionKeyFormatException extends Exception
{
}
<?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 UserNotFoundException extends Exception
{
}
......@@ -18,6 +18,7 @@
* @author Robert Lemke <robert@typo3.org>
*/
use T3o\Ter\Api\ApiUser;
use T3o\Ter\Api\ExtensionKey;
use T3o\Ter\Api\ResultCodes;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -106,7 +107,7 @@ class tx_ter_api
* @param object $extensionInfoData : The general extension information as received by the SOAP server
* @param array $filesData : The array of file data objects as received by the SOAP server
*
* @return object uploadExtensionResult object if upload was successful, otherwise an exception is thrown.
* @return array|object uploadExtensionResult object if upload was successful, otherwise an exception is thrown.
* @throws \T3o\Ter\Exception\Exception
* @throws \T3o\Ter\Exception\VersionExistsException
* @access public
......@@ -131,11 +132,12 @@ class tx_ter_api
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($extensionKey);
if ($extensionKeyRecordArr == false) {
$extensionKeyObject = new ExtensionKey($extensionKey);
if (!$extensionKeyObject->isRegistered()) {
throw new \T3o\Ter\Exception\NotFoundException('Extension "' . $extensionKey . '" does not exist.', ResultCodes::ERROR_UPLOADEXTENSION_EXTENSIONDOESNTEXIST);
}
if (strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername()) && !$user->isAdministrator()) {
if (!$extensionKeyObject->isApiUserOwnerOrAdmin($user)) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_UPLOADEXTENSION_ACCESSDENIED);
}
......@@ -207,12 +209,12 @@ class tx_ter_api
// Make an instance of the api
$instance = GeneralUtility::makeInstance(\tx_ter_api::class);
$accountData = (object)['username' => $username];
// Load extension
$extensionKeyRecordArr = $instance->helperObj->getExtensionKeyRecord(strtolower($extensionInfoData->extensionKey));
if ($extensionKeyRecordArr == false) {
$extensionKeyObject = new ExtensionKey($extensionInfoData->extensionKey);
if (!$extensionKeyObject->isRegistered()) {
throw new \T3o\Ter\Exception\NotFoundException('Extension does not exist.', ResultCodes::ERROR_UPLOADEXTENSION_EXTENSIONDOESNTEXIST);
}
if (strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($username)) {
if (strtolower($extensionKeyObject->getOwner()) !== strtolower($username)) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_UPLOADEXTENSION_ACCESSDENIED);
}
......@@ -272,8 +274,9 @@ class tx_ter_api
ResultCodes::ERROR_DELETEEXTENSION_ACCESS_DENIED
);
}
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($extensionKey);
if ($extensionKeyRecordArr === false) {
$extensionKeyObject = new ExtensionKey($extensionKey);
if (!$extensionKeyObject->isRegistered()) {
throw new \T3o\Ter\Exception\NotFoundException(
'Extension does not exist.',
ResultCodes::ERROR_DELETEEXTENSION_EXTENSIONDOESNTEXIST
......@@ -301,10 +304,13 @@ class tx_ter_api
*/
public function checkExtensionKey($accountData, $extensionKey)
{
if ($this->checkExtensionKey_extensionKeyIsFormallyValid($extensionKey)) {
$resultCode = $this->helperObj->extensionKeyIsAvailable(
$extensionKey
) ? ResultCodes::RESULT_EXTENSIONKEYDOESNOTEXIST : ResultCodes::RESULT_EXTENSIONKEYALREADYEXISTS;
$extensionKeyObject = new ExtensionKey($extensionKey);
if ($extensionKeyObject->isValidExtensionKeyName()) {
if (!$extensionKeyObject->isRegistered()) {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYDOESNOTEXIST;
} else {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYALREADYEXISTS;
}
} else {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYNOTVALID;
}
......@@ -327,15 +333,25 @@ class tx_ter_api
*/
public function registerExtensionKey($accountData, $registerExtensionKeyData)
{
if ($this->checkExtensionKey_extensionKeyIsFormallyValid($registerExtensionKeyData->extensionKey)) {
if ($this->helperObj->extensionKeyIsAvailable($registerExtensionKeyData->extensionKey)) {
$this->registerExtensionKey_writeExtensionKeyInfoToDB($accountData, $registerExtensionKeyData);
$resultCode = ResultCodes::RESULT_EXTENSIONKEYSUCCESSFULLYREGISTERED;
} else {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYALREADYEXISTS;
}
} else {
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
try {
$extensionKeyObject = new ExtensionKey($registerExtensionKeyData->extensionKey);
$extensionKeyObject->registerKey(
$user,
$registerExtensionKeyData->title ?? '',
$registerExtensionKeyData->description ?? '',
$registerExtensionKeyData->ownerUsername ?? ''
);
$resultCode = ResultCodes::RESULT_EXTENSIONKEYSUCCESSFULLYREGISTERED;
} catch (\T3o\Ter\Exception\InvalidExtensionKeyFormatException $e) {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYNOTVALID;
} catch (\T3o\Ter\Exception\UnauthorizedException $e) {
$resultCode = $e->getCode();
} catch (\T3o\Ter\Exception\UserNotFoundException $e) {
$resultCode = ResultCodes::ERROR_GENERAL_USERNOTFOUND;
} catch (\T3o\Ter\Exception\ExtensionKeyAlreadyInUseException $e) {
$resultCode = ResultCodes::RESULT_EXTENSIONKEYALREADYEXISTS;
}
return [
......@@ -358,54 +374,15 @@ class tx_ter_api
*/
public function getExtensionKeys($accountData, $extensionKeyFilterOptions)
{
$extensionKeyDataArr = [];
if (!empty($extensionKeyFilterOptions->username)) {
$whereClause = 'ownerusername = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr(
$extensionKeyFilterOptions->username,
'tx_ter_extensionkeys'
);
}
if (!empty($extensionKeyFilterOptions->title)) {
$whereClause = 'title = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr(
$extensionKeyFilterOptions->title,
'tx_ter_extensionkeys'
);
}
if (!empty($extensionKeyFilterOptions->description)) {
$whereClause = 'description = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr(
$extensionKeyFilterOptions->description,
'tx_ter_extensionkeys'
);
}
if (!empty($extensionKeyFilterOptions->extensionKey)) {
$whereClause = 'extensionkey = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr(
$extensionKeyFilterOptions->extensionKey,
'tx_ter_extensionkeys'
);
}
$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
'extensionkey,title,description,ownerusername',
'tx_ter_extensionkeys',
$whereClause
$extensionKeyDataArr = ExtensionKey::findExtensionKeys(
$extensionKeyFilterOptions->username ?? null,
$extensionKeyFilterOptions->title ?? null,
$extensionKeyFilterOptions->description ?? null,
$extensionKeyFilterOptions->extensionKey ?? null
);
if ($res) {
while (($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))) {
$extensionKeyDataArr[] = $row;
}
$resultCode = ResultCodes::RESULT_GENERAL_OK;
} else {
throw new \T3o\Ter\Exception\InternalServerErrorException(
'Database error while fetching extension keys.',
ResultCodes::ERROR_GENERAL_DATABASEERROR
);
}
return [
'simpleResult' => [
'resultCode' => $resultCode,
'resultCode' => ResultCodes::RESULT_GENERAL_OK,
'resultMessages' => []
],
'extensionKeyData' => $extensionKeyDataArr
......@@ -429,12 +406,9 @@ class tx_ter_api
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($extensionKey);
if (is_array($extensionKeyRecordArr)) {
if (!$user->isAdministrator()
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername())
) {
$extensionKeyObject = new ExtensionKey($extensionKey);
if ($extensionKeyObject->isRegistered()) {
if ($extensionKeyObject->isApiUserOwnerOrAdmin($user)) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_DELETEEXTENSIONKEY_ACCESSDENIED);
}
......@@ -499,18 +473,25 @@ class tx_ter_api
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($modifyExtensionKeyData->extensionKey);
$extensionKeyObject = new ExtensionKey($modifyExtensionKeyData->extensionKey);
if (is_array($extensionKeyRecordArr)) {
if (!$user->isAdministrator()
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername())
) {
if (!$extensionKeyObject->isRegistered()) {
$resultCode = ResultCodes::ERROR_MODIFYEXTENSIONKEY_KEYDOESNOTEXIST;
} else {
try {
$extensionKeyObject->transferOwnership(
$user,