Commit 2fc5057b authored by Matthias Adrowski's avatar Matthias Adrowski
Browse files

Merge branch 'feature_Upgrade-to-TYPO3-11' into 'master'

Upgrade-to-TYPO3-11

See merge request !6
parents 14e84568 450d5acf
......@@ -30,6 +30,7 @@ use Exception;
use SGalinski\SgRest\Service\Authentication\AuthenticationServiceInterface;
use SGalinski\SgRest\Service\DataResolveService;
use SGalinski\SgRest\Service\RegistrationService;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
......@@ -46,7 +47,6 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
* @package SGalinski\SgRest\Controller
*/
abstract class AbstractRestController extends ActionController {
/**
* @var JsonView
*/
......@@ -58,7 +58,7 @@ abstract class AbstractRestController extends ActionController {
protected $defaultViewObjectName = JsonView::class;
/**
* @var string
* @var array
*/
protected $viewFormatToObjectNameMap = [
'json' => JsonView::class
......@@ -157,34 +157,6 @@ abstract class AbstractRestController extends ActionController {
}
}
/**
* Processes the request (additionally we catch any AJAX exceptions to provide better error messages)
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @throws Exception
*/
public function processRequest(RequestInterface $request, ResponseInterface $response): void {
try {
parent::processRequest($request, $response);
} catch (Exception $exception) {
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->critical(
'AbstractRestController::processRequest - Request failed with an exception',
[
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
]
);
$this->response->setStatus($exception->getCode() < 1000 ? $exception->getCode(): 500 );
$this->response->addAdditionalHeaderData($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error');
$this->response->appendContent(json_encode(['message' => $exception->getMessage()]));
}
}
/**
* Prepare the request parameter for the given property name. This is only called for properties of a type that
* inherits from AbstractEntity.
......@@ -244,10 +216,10 @@ abstract class AbstractRestController extends ActionController {
* @throws Exception
*/
public function errorAction(): void {
$validationResults = $this->arguments->getValidationResults();
$validationResults = $this->arguments->validate();
$errors = $validationResults->getFlattenedErrors();
if (count($errors)) {
if (is_countable($errors) ? count($errors) : 0) {
$message = 'The request could not be completed due to a conflict with the current state of the resource. ';
$violatedProperties = $this->getViolatedProperties($errors);
......@@ -297,5 +269,4 @@ abstract class AbstractRestController extends ActionController {
$this->view->assign('data', $data);
}
}
......@@ -38,7 +38,6 @@ use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
* Class AuthenticationController
*/
class AuthenticationController extends AbstractRestController implements LoggerAwareInterface {
use LoggerAwareTrait;
/**
......@@ -57,7 +56,6 @@ class AuthenticationController extends AbstractRestController implements LoggerA
* Checks if there is a logged in frontend user and gives out a token
*/
public function postGetbearertokenAction(): void {
$loggedInUser = $GLOBALS['TSFE']->fe_user->user;
if ($loggedInUser !== NULL) {
......@@ -65,7 +63,6 @@ class AuthenticationController extends AbstractRestController implements LoggerA
// if the user doesn't have permission for any access group, no need for him to get a token
if ($accessGroups !== '') {
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('sg_rest');
$expirationTime = $extConf['tokenExpirationTime'];
......@@ -88,7 +85,7 @@ class AuthenticationController extends AbstractRestController implements LoggerA
}
// Clear the session of the logged in user again
if($GLOBALS['TSFE']->fe_user instanceof FrontendUserAuthentication){
if ($GLOBALS['TSFE']->fe_user instanceof FrontendUserAuthentication) {
$GLOBALS['TSFE']->fe_user->removeSessionData();
$GLOBALS['TSFE']->fe_user = NULL;
}
......@@ -105,8 +102,7 @@ class AuthenticationController extends AbstractRestController implements LoggerA
['user' => $loggedInUser]
);
}
} else if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
} elseif ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
$this->logger->log(
LogLevel::ERROR,
'No authenticated user found while trying to retrieve a bearer token.',
......@@ -119,5 +115,4 @@ class AuthenticationController extends AbstractRestController implements LoggerA
403
);
}
}
......@@ -50,7 +50,7 @@ class FrontendUser extends ExtbaseFrontendUser {
/**
* Setter for auth token
*
* @param $token
* @param string $token
* @return void
*/
public function setAuthToken($token): void {
......@@ -100,5 +100,3 @@ class FrontendUser extends ExtbaseFrontendUser {
$this->testMode = $testMode;
}
}
?>
......@@ -35,7 +35,6 @@ use TYPO3\CMS\Extbase\Domain\Model\FileReference;
* @package SGalinski\SgRest\Events
*/
class BeforeComposeFileReferenceArrayEvent {
/**
* @var FileReference
*/
......
......@@ -44,7 +44,6 @@ use TYPO3\CMS\Core\Http\Response;
* @package SGalinski\SgRest\Middleware
*/
abstract class AbstractRestMiddleware implements LoggerAwareInterface, MiddlewareInterface {
use LoggerAwareTrait;
/**
......@@ -148,8 +147,8 @@ abstract class AbstractRestMiddleware implements LoggerAwareInterface, Middlewar
if (isset($this->pathSegments['entity'])) {
$controllerName .= 'Rest\\' . ucfirst($this->pathSegments['apiKey']) . '\\' . ucfirst(
$this->pathSegments['entity']
);
$this->pathSegments['entity']
);
}
return $controllerName;
......@@ -158,22 +157,33 @@ abstract class AbstractRestMiddleware implements LoggerAwareInterface, Middlewar
/**
* Method returns the requested action for the rest controller. If no
*
* @param $method
* @param string $method
* @return string
*/
protected function getCallableActionName($method): string {
if ($method === 'POST') {
return 'post' . mb_strtoupper(mb_substr($this->pathSegments['verb'], 0, 1)) . mb_substr(
$this->pathSegments['verb'], 1
);
$this->pathSegments['verb'],
1
);
}
return 'get' . mb_strtoupper(mb_substr($this->pathSegments['verb'], 0, 1)) . mb_substr(
$this->pathSegments['verb'], 1
);
$this->pathSegments['verb'],
1
);
}
/**
*
* @return string
* @throws Exception
*/
protected function getActionName(): string {
return $this->pathSegments['verb'];
}
/**
* @param $data
* @param mixed $data
* @param int $statusCode
* @return Response
*/
......
......@@ -42,7 +42,6 @@ use TYPO3\CMS\Core\Utility\HttpUtility;
* @package SGalinski\SgRest\Middleware
*/
class RestAuthenticator extends AbstractRestMiddleware {
/**
* Process an incoming server request.
*
......@@ -56,7 +55,6 @@ class RestAuthenticator extends AbstractRestMiddleware {
* @throws \Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$queryParams = $request->getQueryParams();
if (!isset($queryParams[self::argumentNamespace])) {
......@@ -64,7 +62,9 @@ class RestAuthenticator extends AbstractRestMiddleware {
}
$this->requestSegments = GeneralUtility::trimExplode(
'/', $queryParams[self::argumentNamespace]['request'], TRUE
'/',
$queryParams[self::argumentNamespace]['request'],
TRUE
);
$this->requestHeaders = $request->getHeaders();
......@@ -83,7 +83,8 @@ class RestAuthenticator extends AbstractRestMiddleware {
try {
// @todo: this currently returns a 500 for a permission question?
$httpPermissions = $this->registrationService->getHttpPermissionsForEntity(
$this->pathSegments['entity'], $apiKey
$this->pathSegments['entity'],
$apiKey
);
} catch (\RuntimeException $exception) {
if (in_array($exception->getCode(), $this->reasonableExceptionCodes, TRUE)) {
......@@ -102,11 +103,9 @@ class RestAuthenticator extends AbstractRestMiddleware {
* when the client requests a bearer token, we don't need to do access checks etc. the user verification is done by the AuthServices
*/
if (!($this->pathSegments['entity'] === 'authentication' && $actionName === 'postGetbearertoken' && $httpMethod === 'POST')) {
$authenticated = $this->authenticationService->verifyRequest($this->requestHeaders);
if (!$authenticated) {
if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
$this->logger->log(
LogLevel::ERROR,
......@@ -125,7 +124,6 @@ class RestAuthenticator extends AbstractRestMiddleware {
$verifiedAccess = $this->authenticationService->verifyUserAccess($apiKey);
if (!$verifiedAccess) {
if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
$this->logger->log(
LogLevel::ERROR,
......@@ -139,12 +137,12 @@ class RestAuthenticator extends AbstractRestMiddleware {
}
return $this->createExceptionJsonResponse(
'You tried to access an object where you do not have the necessary permissions.', 403
'You tried to access an object where you do not have the necessary permissions.',
403
);
}
if ($httpMethod === 'DELETE' && $this->pathSegments['verb'] !== '' && !$httpPermissions['deleteForVerbs']) {
if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
$this->logger->log(
LogLevel::ERROR,
......@@ -162,7 +160,8 @@ class RestAuthenticator extends AbstractRestMiddleware {
}
return $this->createExceptionJsonResponse(
'The DELETE method is not permitted for the given path.', 405
'The DELETE method is not permitted for the given path.',
405
);
}
......@@ -171,7 +170,6 @@ class RestAuthenticator extends AbstractRestMiddleware {
($httpMethod === 'PATCH' && $identifier <= 0 && !$httpPermissions['patchWithIdentifier']) ||
($httpMethod === 'POST' && $identifier > 0 && !$httpPermissions['postWithIdentifier'])
) {
if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
$this->logger->log(
LogLevel::ERROR,
......@@ -189,7 +187,8 @@ class RestAuthenticator extends AbstractRestMiddleware {
}
return $this->createExceptionJsonResponse(
'The ' . $httpMethod . ' method is not permitted for the given path.', 405
'The ' . $httpMethod . ' method is not permitted for the given path.',
405
);
}
......
......@@ -33,7 +33,9 @@ use SGalinski\SgRest\Service\DataResolveService;
use SGalinski\SgRest\Utility\PathUtility;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\TypoScriptAspect;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -45,7 +47,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
* @package SGalinski\SgRest\Middleware
*/
class RestDispatcher extends AbstractRestMiddleware {
/**
* Process an incoming server request.
*
......@@ -59,7 +60,6 @@ class RestDispatcher extends AbstractRestMiddleware {
* @throws \Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$queryParams = $request->getQueryParams();
if (!isset($queryParams[self::argumentNamespace])) {
......@@ -69,7 +69,9 @@ class RestDispatcher extends AbstractRestMiddleware {
$parsedBody = $request->getParsedBody();
$this->requestSegments = GeneralUtility::trimExplode(
'/', $queryParams[self::argumentNamespace]['request'], TRUE
'/',
$queryParams[self::argumentNamespace]['request'],
TRUE
);
$this->requestHeaders = $request->getHeaders();
......@@ -93,6 +95,7 @@ class RestDispatcher extends AbstractRestMiddleware {
$extensionName = $this->accessGroup['extensionName'];
$className = $this->getClassName();
$controllerName = $this->getControllerName();
$httpMethod = $request->getMethod();
$actionName = $this->getCallableActionName($httpMethod);
......@@ -122,14 +125,12 @@ class RestDispatcher extends AbstractRestMiddleware {
*/
$typoscriptPluginConfiguration = 'sgRest.10.extensionName=' . $extensionName . PHP_EOL
. 'sgRest.10.vendorName=' . $vendorName . PHP_EOL
. 'sgRest.10.controller=' . $className . PHP_EOL
. 'sgRest.10.controller=' . $controllerName . PHP_EOL
. 'sgRest.10.action=' . $actionName . PHP_EOL
. 'sgRest.10.pluginName=Rest' . $apiKey . PHP_EOL
. 'sgRest.10.switchableControllerActions {' . PHP_EOL
. ' ' . $className . ' { ' . PHP_EOL
. ' ' . $className . ' = ' . $actionName . PHP_EOL
. ' }' . PHP_EOL
. '}' . PHP_EOL;
. 'sgRest.10.pluginName=Rest' . $apiKey . PHP_EOL;
# Set Action Name in $GLOBALS, will be read from \TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager::getControllerConfiguration
# to determin currently allowed Controller::Actions
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins']['Rest' . $apiKey]['controllers'][$className]['actions'][0] = $actionName;
ExtensionManagementUtility::addTypoScriptSetup($typoscriptPluginConfiguration);
$pluginNamespace = 'tx_' . strtolower($extensionName) . '_rest';
......@@ -150,13 +151,22 @@ class RestDispatcher extends AbstractRestMiddleware {
}
}
$queryParams[$pluginNamespace]['controller'] = $className;
$queryParams[$pluginNamespace]['controller'] = $controllerName;
$queryParams[$pluginNamespace]['action'] = $actionName;
$queryParams[$pluginNamespace]['format'] = $format;
$queryParams[$pluginNamespace]['extensionName'] = $extensionName;
$queryParams[$pluginNamespace]['extension'] = $extensionName;
$queryParams[$pluginNamespace]['vendor'] = $vendorName;
$queryParams[$pluginNamespace]['vendorName'] = $vendorName;
# Add both cases, with and without apiKey, since we seem to have inconsistencies
# with ususal and bearerToken alls
$queryParams[$pluginNamespace . $apiKey]['controller'] = $controllerName;
$queryParams[$pluginNamespace . $apiKey]['action'] = $actionName;
$queryParams[$pluginNamespace . $apiKey]['format'] = $format;
$queryParams[$pluginNamespace . $apiKey]['extensionName'] = $extensionName;
$queryParams[$pluginNamespace . $apiKey]['extension'] = $extensionName;
$queryParams[$pluginNamespace . $apiKey]['vendor'] = $vendorName;
$queryParams[$pluginNamespace . $apiKey]['vendorName'] = $vendorName;
if ($identifier) {
$queryParams[$pluginNamespace . $apiKey]['identifier'] = $identifier;
......@@ -186,6 +196,42 @@ class RestDispatcher extends AbstractRestMiddleware {
);
}
return $handler->handle($request);
try {
return $handler->handle($request);
} catch (\Exception $exception) {
return $this->getErrorResponse($exception);
}
}
/**
* Builds an Error Repsonse from catched exception
*
* @param \Exception $exception
* @return Response
*/
private function getErrorResponse(\Exception $exception): Response {
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(self::class);
$logger->critical(
'AbstractAjaxController::processRequest - Request failed with an exception',
[
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
]
);
if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) {
$response = new Response();
$response->setStatus(500);
$response->addAdditionalHeaderData($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error');
$response->appendContent($exception->getMessage());
} else {
$message = $exception->getMessage();
$stream = fopen('php://temp', 'r+');
fwrite($stream, $message);
$response = new Response($stream, 500);
}
return $response;
}
}
......@@ -37,7 +37,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
* @package SGalinski\SgRest\Service\Authentication
*/
abstract class AbstractAuthenticationService implements AuthenticationServiceInterface, LoggerAwareInterface {
use LoggerAwareTrait;
/**
......@@ -61,7 +60,7 @@ abstract class AbstractAuthenticationService implements AuthenticationServiceInt
/**
* Verify if the authenticated user has access to the given apikey.
*
* @param $apiKey
* @param string $apiKey
* @return bool
*/
public function verifyUserAccess($apiKey): bool {
......
......@@ -32,7 +32,6 @@ namespace SGalinski\SgRest\Service\Authentication;
* @package SGalinski\SgRest\Service\Authentication
*/
interface AuthenticationServiceInterface {
/**
* Tries to authenticate a request with the given request headers
*
......@@ -44,7 +43,7 @@ interface AuthenticationServiceInterface {
/**
* Verify if the authenticated user has access to the given apikey.
*
* @param $apiKey
* @param string $apiKey
* @return bool
*/
public function verifyUserAccess(string $apiKey): bool;
......
......@@ -28,8 +28,8 @@ namespace SGalinski\SgRest\Service\Authentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class BasicAuthenticationService
......@@ -37,13 +37,11 @@ use TYPO3\CMS\Core\SingletonInterface;
* @package SGalinski\SgRest\Service\Authentication
*/
class BasicAuthenticationService extends AbstractAuthenticationService implements SingletonInterface {
/**
* @param array $requestHeaders
* @return bool
*/
public function verifyRequest(array $requestHeaders): bool {
if (isset($requestHeaders['authtoken'][0]) && !empty($requestHeaders['authtoken'][0])) {
$authToken = $requestHeaders['authtoken'][0];
return ($authToken !== '' && $this->verifyAuthToken($authToken));
......@@ -59,7 +57,6 @@ class BasicAuthenticationService extends AbstractAuthenticationService implement
* @return boolean
*/
protected function verifyAuthToken($authToken): bool {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_users');
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
......@@ -72,7 +69,7 @@ class BasicAuthenticationService extends AbstractAuthenticationService implement
)
)
->execute()
->fetchAll()[0];
->fetchAllAssociative()[0];
if (!empty($user)) {
$this->authenticatedUser = $user;
......@@ -90,7 +87,6 @@ class BasicAuthenticationService extends AbstractAuthenticationService implement
* @throws \Exception
*/
public function generateAndUpdateAuthToken($userId): void {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(
'fe_users'
);
......@@ -107,7 +103,7 @@ class BasicAuthenticationService extends AbstractAuthenticationService implement
)
)
->execute()
->fetchAll()[0];
->fetchAllAssociative()[0];
if (!$frontendUser) {
throw new \Exception('The requested user does not exist or is invalid', 500);
......
......@@ -41,7 +41,6 @@ use TYPO3\CMS\Extbase\Object\Exception;
* @package SGalinski\SgRest\Service\Authentication
*/
class BearerAuthenticationService extends AbstractAuthenticationService implements SingletonInterface {
/**
* @var BearerTokenService
*/
......@@ -65,16 +64,14 @@ class BearerAuthenticationService extends AbstractAuthenticationService implemen
}
/**
* @param $bearerToken
* @param string $bearerToken
* @return bool
* @throws Exception
*/
protected function verifyBearerToken($bearerToken): bool {
$verified = $this->bearerTokenService->verifyToken($bearerToken);
if ($verified) {
$decodedToken = $this->bearerTokenService->decodeToken($bearerToken);
if ($decodedToken && $decodedToken->user > 0) {
......@@ -96,7 +93,7 @@ class BearerAuthenticationService extends AbstractAuthenticationService implemen
)
)
->execute()
->fetchAll()[0];
->fetchAllAssociative()[0];
if (!empty($user)) {
$this->authenticatedUser = $user;
......
......@@ -26,10 +26,10 @@ namespace SGalinski\SgRest\Service;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Extbase\Domain\Model\FrontendUser as ExtbaseFrontendUser;
use DomainException;
use TYPO3\CMS\Core\SingletonInterface;
use \DomainException;
use \UnexpectedValueException;
use TYPO3\CMS\Extbase\Domain\Model\FrontendUser as ExtbaseFrontendUser;
use UnexpectedValueException;
/**
* This class handles the JWT / Bearer Token generation and validation for the BearerAuthenticationService
......@@ -39,7 +39,6 @@ use \UnexpectedValueException;
* @package SGalinski\SgRest\Service
*/
class BearerTokenService implements SingletonInterface {
/**
* stores the local private key
* @var string
......@@ -66,7 +65,7 @@ class BearerTokenService implements SingletonInterface {
* Will default to PHP time() value if null.
* @var int
*/
public $timestamp = NULL;
public $timestamp;
/**
* @var array
......@@ -96,9 +95,8 @@ class BearerTokenService implements SingletonInterface {
* @return boolean
*/
public function verifyToken($bearerToken): bool {
if (!empty($bearerToken)) {
$payload = $this->decodeToken($bearerToken, $this->privateKey, TRUE);
$payload = $this->decodeToken($bearerToken, $this->privateKey);
if ($payload !== NULL) {
$expire = $payload->exp;
......
......@@ -29,6 +29,7 @@ namespace SGalinski\SgRest\Service;
use Exception;
use SGalinski\SgRest\Events\BeforeComposeFileReferenceArrayEvent;
use SGalinski\SgRest\Utility\PathUtility;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\Resource\AbstractFile;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\SingletonInterface;
......@@ -39,14 +40,12 @@ use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
/**