[!!!][FEATURE] Introduce PSR-7-based Routing for Backend AJAX Requests
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Http / AjaxRequestHandler.php
index 9e1acbc..df08ed5 100644 (file)
@@ -14,9 +14,16 @@ namespace TYPO3\CMS\Backend\Http;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
 use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Http\Response;
 use TYPO3\CMS\Core\Http\RequestHandlerInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Backend\Routing\Route;
+use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
+use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 
 /**
@@ -44,13 +51,11 @@ class AjaxRequestHandler implements RequestHandlerInterface {
         * @var array
         */
        protected $publicAjaxIds = array(
-               'BackendLogin::login',
-               'BackendLogin::logout',
-               'BackendLogin::refreshLogin',
-               'BackendLogin::isTimedOut',
-               'BackendLogin::getChallenge',
-               'BackendLogin::getRsaPublicKey',
-               'RsaEncryption::getRsaPublicKey'
+               '/ajax/login',
+               '/ajax/logout',
+               '/ajax/login/refresh',
+               '/ajax/login/timedout',
+               '/ajax/rsa/publickey'
        );
 
        /**
@@ -74,47 +79,17 @@ class AjaxRequestHandler implements RequestHandlerInterface {
 
                // used for backwards-compatibility
                $GLOBALS['ajaxID'] = $ajaxID;
-               $this->boot($ajaxID);
-
-               // Finding the script path from the registry
-               $ajaxRegistryEntry = isset($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID]) ? $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID] : NULL;
-               $ajaxScript = NULL;
-               $csrfTokenCheck = FALSE;
-               if ($ajaxRegistryEntry !== NULL && is_array($ajaxRegistryEntry) && isset($ajaxRegistryEntry['callbackMethod'])) {
-                       $ajaxScript = $ajaxRegistryEntry['callbackMethod'];
-                       $csrfTokenCheck = $ajaxRegistryEntry['csrfTokenCheck'];
-               }
-
-               // Instantiating the AJAX object
-               $ajaxObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\AjaxRequestHandler::class, $ajaxID);
-               $ajaxParams = array('request' => $request);
-
-               // Evaluating the arguments and calling the AJAX method/function
-               if (empty($ajaxID)) {
-                       $ajaxObj->setError('No valid ajaxID parameter given.');
-               } elseif (empty($ajaxScript)) {
-                       $ajaxObj->setError('No backend function registered for ajaxID "' . $ajaxID . '".');
-               } else {
-                       $success = TRUE;
-                       $tokenIsValid = TRUE;
-                       if ($csrfTokenCheck) {
-                               $ajaxToken = $request->getParsedBody()['ajaxToken'] ?: $request->getQueryParams()['ajaxToken'];
-                               $tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken($ajaxToken, 'ajaxCall', $ajaxID);
-                       }
-                       if ($tokenIsValid) {
-                               // Cleanup global variable space
-                               unset($csrfTokenCheck, $ajaxRegistryEntry, $tokenIsValid, $success);
-                               $success = GeneralUtility::callUserFunction($ajaxScript, $ajaxParams, $ajaxObj, FALSE, TRUE);
-                       } else {
-                               $ajaxObj->setError('Invalid CSRF token detected for ajaxID "' . $ajaxID . '"!');
-                       }
-                       if ($success === FALSE) {
-                               $ajaxObj->setError('Registered backend function for ajaxID "' . $ajaxID . '" was not found.');
-                       }
+               $request = $request->withAttribute('routePath', $ajaxID);
+               $proceedIfNoUserIsLoggedIn = $this->isLoggedInBackendUserRequired($ajaxID);
+               $this->boot($proceedIfNoUserIsLoggedIn);
+
+               try {
+                       // Backend Routing - check if a valid route is there, and dispatch
+                       return $this->dispatch($request);
+               } catch (ResourceNotFoundException $e) {
+                       // no Route found, fallback to the traditional AJAX request
                }
-
-               // Outputting the content (and setting the X-JSON-Header)
-               return $ajaxObj->render();
+               return $this->dispatchTraditionalAjaxRequest($request);
        }
 
        /**
@@ -138,14 +113,22 @@ class AjaxRequestHandler implements RequestHandlerInterface {
        }
 
        /**
-        * Start the Backend bootstrap part
+        * Check if the user is required for the request
+        * If we're trying to do an ajax login, don't require a user
         *
-        * @param string $ajaxId Contains the string of the ajaxId used
+        * @param string $ajaxId the Ajax ID to check against
+        * @return bool whether the request can proceed without a login required
         */
-       protected function boot($ajaxId) {
-               // If we're trying to do an ajax login, don't require a user
-               $proceedIfNoUserIsLoggedIn = in_array($ajaxId, $this->publicAjaxIds, TRUE);
+       protected function isLoggedInBackendUserRequired($ajaxId) {
+               return in_array($ajaxId, $this->publicAjaxIds, TRUE);
+       }
 
+       /**
+        * Start the Backend bootstrap part
+        *
+        * @param bool $proceedIfNoUserIsLoggedIn a flag if a backend user is required
+        */
+       protected function boot($proceedIfNoUserIsLoggedIn) {
                $this->bootstrap
                        ->checkLockedBackendAndRedirectOrDie($proceedIfNoUserIsLoggedIn)
                        ->checkBackendIpOrDie()
@@ -161,4 +144,87 @@ class AjaxRequestHandler implements RequestHandlerInterface {
                        ->initializeOutputCompression()
                        ->sendHttpHeaders();
        }
+
+       /**
+        * Creates a response object with JSON headers automatically, and then dispatches to the correct route
+        *
+        * @param ServerRequestInterface $request
+        * @return ResponseInterface $response
+        * @throws ResourceNotFoundException if no valid route was found
+        * @throws RouteNotFoundException if the request could not be verified
+        */
+       protected function dispatch(ServerRequestInterface $request) {
+               /** @var Response $response */
+               $response = GeneralUtility::makeInstance(Response::class, 'php://temp', 200, [
+                       'Content-type' => 'application/json; charset=utf-8',
+                       'X-JSON' => 'true'
+               ]);
+
+               /** @var RouteDispatcher $dispatcher */
+               $dispatcher = GeneralUtility::makeInstance(RouteDispatcher::class);
+               return $dispatcher->dispatch($request, $response);
+       }
+
+       /**
+        * Calls the ajax callback method registered in TYPO3_CONF_VARS[BE][AJAX]
+        *
+        * @param ServerRequestInterface $request
+        * @return ResponseInterface
+        */
+       protected function dispatchTraditionalAjaxRequest($request) {
+               $ajaxID = $request->getAttribute('routePath');
+               // Finding the script path from the registry
+               $ajaxRegistryEntry = isset($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID]) ? $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID] : NULL;
+               $ajaxScript = NULL;
+               $csrfTokenCheck = FALSE;
+               if ($ajaxRegistryEntry !== NULL && is_array($ajaxRegistryEntry) && isset($ajaxRegistryEntry['callbackMethod'])) {
+                       $ajaxScript = $ajaxRegistryEntry['callbackMethod'];
+                       $csrfTokenCheck = $ajaxRegistryEntry['csrfTokenCheck'];
+               }
+
+               // Instantiating the AJAX object
+               /** @var \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj */
+               $ajaxObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\AjaxRequestHandler::class, $ajaxID);
+               $ajaxParams = array('request' => $request);
+
+               // Evaluating the arguments and calling the AJAX method/function
+               if (empty($ajaxID)) {
+                       $ajaxObj->setError('No valid ajaxID parameter given.');
+               } elseif (empty($ajaxScript)) {
+                       $ajaxObj->setError('No backend function registered for ajaxID "' . $ajaxID . '".');
+               } elseif ($csrfTokenCheck && !$this->isValidRequest($request)) {
+                       $ajaxObj->setError('Invalid CSRF token detected for ajaxID "' . $ajaxID . '"!');
+               } else {
+                       $success = GeneralUtility::callUserFunction($ajaxScript, $ajaxParams, $ajaxObj, FALSE, TRUE);
+                       if ($success === FALSE) {
+                               $ajaxObj->setError('Registered backend function for ajaxID "' . $ajaxID . '" was not found.');
+                       }
+               }
+
+               // Outputting the content (and setting the X-JSON-Header)
+               return $ajaxObj->render();
+       }
+
+       /**
+        * Wrapper method for static form protection utility
+        *
+        * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
+        */
+       protected function getFormProtection() {
+               return FormProtectionFactory::get();
+       }
+
+       /**
+        * Checks if the request token is valid. This is checked to see if the route is really
+        * created by the same instance. Should be called for all routes in the backend except
+        * for the ones that don't require a login.
+        *
+        * @param ServerRequestInterface $request
+        * @return bool
+        * @see \TYPO3\CMS\Backend\Routing\UriBuilder where the token is generated.
+        */
+       protected function isValidRequest(ServerRequestInterface $request) {
+               $token = (string)(isset($request->getParsedBody()['ajaxToken']) ? $request->getParsedBody()['ajaxToken'] : $request->getQueryParams()['ajaxToken']);
+               return $this->getFormProtection()->validateToken($token, 'ajaxCall', $request->getAttribute('routePath'));
+       }
 }