Commit 3f0fb622 authored by Benni Mack's avatar Benni Mack Committed by Andreas Fernandez
Browse files

[!!!][FEATURE] Introduce PSR-7-based Routing for Backend AJAX Requests

The AjaxRequestHandler now first checks in the Router if an AJAX
route exists. A new flag "ajax" in the routing mechanism allows to call
ajax-based URLs which are then handed to the AJAX Request Handler.

All controllers now receive proper Request and Response objects.

All previous logic still works, but can slowly be migrated to the Routing
concept.

Resolves: #69916
Releases: master
Change-Id: I1e67d5a341a4dd2769247531246c9e1fad900c76
Reviewed-on: http://review.typo3.org/43365

Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <typo3@scripting-base.de>
parent 5159e538
......@@ -14,8 +14,9 @@ namespace TYPO3\CMS\Backend;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
/**
* This is the ajax handler for backend login after timeout.
......@@ -29,102 +30,111 @@ class AjaxLoginHandler {
* a BE user and reset the timer and hide the login window.
* If it was unsuccessful, we display that and show the login box again.
*
* @param array $parameters Parameters (not used)
* @param AjaxRequestHandler $ajaxObj The calling parent AJAX object
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function login(array $parameters, AjaxRequestHandler $ajaxObj) {
public function loginAction(ServerRequestInterface $request, ResponseInterface $response) {
if ($this->isAuthorizedBackendSession()) {
$json = array('success' => TRUE);
$result = ['success' => TRUE];
if ($this->hasLoginBeenProcessed()) {
$formProtection = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get();
$formProtection->setSessionTokenFromRegistry();
$formProtection->persistSessionToken();
}
} else {
$json = array('success' => FALSE);
$result = ['success' => FALSE];
}
$ajaxObj->addContent('login', $json);
$ajaxObj->setContentFormat('json');
}
/**
* Checks if a user is logged in and the session is active.
*
* @return bool
*/
protected function isAuthorizedBackendSession() {
$backendUser = $this->getBackendUser();
return $backendUser !== NULL && $backendUser instanceof BackendUserAuthentication && isset($backendUser->user['uid']);
}
/**
* Check whether the user was already authorized or not
*
* @return bool
*/
protected function hasLoginBeenProcessed() {
$loginFormData = $this->getBackendUser()->getLoginFormData();
return $loginFormData['status'] === 'login' && !empty($loginFormData['uname']) && !empty($loginFormData['uident']);
$response->getBody()->write(json_encode(['login' => $result]));
return $response;
}
/**
* Logs out the current BE user
*
* @param array $parameters Parameters (not used)
* @param AjaxRequestHandler $ajaxObj The calling parent AJAX object
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function logout(array $parameters, AjaxRequestHandler $ajaxObj) {
public function logoutAction(ServerRequestInterface $request, ResponseInterface $response) {
$backendUser = $this->getBackendUser();
$backendUser->logoff();
$ajaxObj->addContent('logout', array(
'success' => !isset($backendUser->user['uid']))
);
$ajaxObj->setContentFormat('json');
$response->getBody()->write(json_encode([
'logout' => [
'success' => !isset($backendUser->user['uid'])
]
]));
return $response;
}
/**
* Refreshes the login without needing login information. We just refresh the session.
*
* @param array $parameters Parameters (not used)
* @param AjaxRequestHandler $ajaxObj The calling parent AJAX object
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function refreshLogin(array $parameters, AjaxRequestHandler $ajaxObj) {
public function refreshAction(ServerRequestInterface $request, ResponseInterface $response) {
$this->getBackendUser()->checkAuthentication();
$ajaxObj->addContent('refresh', array('success' => TRUE));
$ajaxObj->setContentFormat('json');
$response->getBody()->write(json_encode([
'refresh' => [
'success' => TRUE
]
]));
return $response;
}
/**
* Checks if the user session is expired yet
*
* @param array $parameters Parameters (not used)
* @param AjaxRequestHandler $ajaxObj The calling parent AJAX object
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function isTimedOut(array $parameters, AjaxRequestHandler $ajaxObj) {
$ajaxObj->setContentFormat('json');
$response = array(
public function isTimedOutAction(ServerRequestInterface $request, ResponseInterface $response) {
$session = [
'timed_out' => FALSE,
'will_time_out' => FALSE,
'locked' => FALSE
);
];
$backendUser = $this->getBackendUser();
if (@is_file(PATH_typo3conf . 'LOCK_BACKEND')) {
$response['locked'] = TRUE;
$session['locked'] = TRUE;
} elseif (!isset($backendUser->user['uid'])) {
$response['timed_out'] = TRUE;
$session['timed_out'] = TRUE;
} else {
$backendUser->fetchUserSession(TRUE);
$ses_tstamp = $backendUser->user['ses_tstamp'];
$timeout = $backendUser->auth_timeout_field;
// If 120 seconds from now is later than the session timeout, we need to show the refresh dialog.
// 120 is somewhat arbitrary to allow for a little room during the countdown and load times, etc.
$response['will_time_out'] = $GLOBALS['EXEC_TIME'] >= $ses_tstamp + $timeout - 120;
$session['will_time_out'] = $GLOBALS['EXEC_TIME'] >= $ses_tstamp + $timeout - 120;
}
$ajaxObj->addContent('login', $response);
$response->getBody()->write(json_encode(['login' => $session]));
return $response;
}
/**
* Checks if a user is logged in and the session is active.
*
* @return bool
*/
protected function isAuthorizedBackendSession() {
$backendUser = $this->getBackendUser();
return $backendUser !== NULL && $backendUser instanceof BackendUserAuthentication && isset($backendUser->user['uid']);
}
/**
* Check whether the user was already authorized or not
*
* @return bool
*/
protected function hasLoginBeenProcessed() {
$loginFormData = $this->getBackendUser()->getLoginFormData();
return $loginFormData['status'] === 'login' && !empty($loginFormData['uname']) && !empty($loginFormData['uident']);
}
/**
......
......@@ -14,11 +14,12 @@ namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\PreparedStatement;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
......@@ -193,13 +194,16 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
/**
* Renders the menu so that it can be returned as response to an AJAX call
*
* @param array $params Array of parameters from the AJAX interface, currently unused
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj Object of type AjaxRequestHandler
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function renderAjaxMenu($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
public function menuAction(ServerRequestInterface $request, ResponseInterface $response) {
$menuContent = $this->getDropDown();
$ajaxObj->addContent('shortcutMenu', $menuContent);
$response->getBody()->write($menuContent);
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
return $response;
}
/**
......@@ -417,15 +421,18 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
}
/**
* gets the available shortcut groups, renders a form so it can be saved lateron
* Fetches the available shortcut groups, renders a form so it can be saved later on, usually called via AJAX
*
* @param array $params Array of parameters from the AJAX interface, currently unused
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj Object of type AjaxRequestHandler
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface the full HTML for the form
*/
public function getAjaxShortcutEditForm($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
$selectedShortcutId = (int)GeneralUtility::_GP('shortcutId');
$selectedShortcutGroupId = (int)GeneralUtility::_GP('shortcutGroup');
public function editFormAction(ServerRequestInterface $request, ResponseInterface $response) {
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$selectedShortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
$selectedShortcutGroupId = (int)(isset($parsedBody['shortcutGroup']) ? $parsedBody['shortcutGroup'] : $queryParams['shortcutGroup']);
$selectedShortcut = $this->getShortcutById($selectedShortcutId);
$shortcutGroups = $this->shortcutGroups;
......@@ -457,45 +464,52 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
<input type="button" class="btn btn-success shortcut-form-save" value="Save">
</form>';
$ajaxObj->addContent('data', $content);
$response->getBody()->write($content);
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
return $response;
}
/**
* Deletes a shortcut through an AJAX call
*
* @param array $params Array of parameters from the AJAX interface, currently unused
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj Object of type AjaxRequestHandler
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function deleteAjaxShortcut($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
public function removeShortcutAction(ServerRequestInterface $request, ResponseInterface $response) {
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$databaseConnection = $this->getDatabaseConnection();
$shortcutId = (int)GeneralUtility::_POST('shortcutId');
$shortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
$fullShortcut = $this->getShortcutById($shortcutId);
$ajaxReturn = 'failed';
$success = FALSE;
if ($fullShortcut['raw']['userid'] == $this->getBackendUser()->user['uid']) {
$databaseConnection->exec_DELETEquery('sys_be_shortcuts', 'uid = ' . $shortcutId);
if ($databaseConnection->sql_affected_rows() == 1) {
$ajaxReturn = 'deleted';
if ($databaseConnection->sql_affected_rows() === 1) {
$success = TRUE;
}
}
$ajaxObj->addContent('delete', $ajaxReturn);
$response->getBody()->write(json_encode(['success' => $success]));
return $response;
}
/**
* Creates a shortcut through an AJAX call
*
* @param array $params Array of parameters from the AJAX interface, currently unused
* @param AjaxRequestHandler $ajaxObj Oject of type AjaxRequestHandler
*
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function createAjaxShortcut($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
public function createShortcutAction(ServerRequestInterface $request, ResponseInterface $response) {
$languageService = $this->getLanguageService();
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
// Default name
$shortcutName = 'Shortcut';
$shortcutNamePrepend = '';
$url = GeneralUtility::_POST('url');
$url = isset($parsedBody['url']) ? $parsedBody['url'] : $queryParams['url'];
// Determine shortcut type
$url = rawurldecode($url);
......@@ -545,20 +559,19 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
}
}
$this->tryAddingTheShortcut($ajaxObj, $url, $shortcutName);
return $this->tryAddingTheShortcut($response, $url, $shortcutName);
}
}
/**
* Try to adding a shortcut
*
* @param AjaxRequestHandler $ajaxObj Oject of type AjaxRequestHandler
* @param ResponseInterface $response
* @param string $url
* @param string $shortcutName
*
* @return void
* @return ResponseInterface
*/
protected function tryAddingTheShortcut(AjaxRequestHandler $ajaxObj, $url, $shortcutName) {
protected function tryAddingTheShortcut(ResponseInterface $response, $url, $shortcutName) {
$module = GeneralUtility::_POST('module');
$shortcutCreated = 'failed';
......@@ -570,7 +583,9 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
}
}
$ajaxObj->addContent('create', $shortcutCreated);
$response->getBody()->write($shortcutCreated);
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
return $response;
}
/**
......@@ -640,16 +655,19 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
* Gets called when a shortcut is changed, checks whether the user has
* permissions to do so and saves the changes if everything is ok
*
* @param array $params Array of parameters from the AJAX interface, currently unused
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj Object of type AjaxRequestHandler
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function setAjaxShortcut($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
public function saveFormAction(ServerRequestInterface $request, ResponseInterface $response) {
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$databaseConnection = $this->getDatabaseConnection();
$backendUser = $this->getBackendUser();
$shortcutId = (int)GeneralUtility::_POST('shortcutId');
$shortcutName = strip_tags(GeneralUtility::_POST('shortcutTitle'));
$shortcutGroupId = (int)GeneralUtility::_POST('shortcutGroup');
$shortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
$shortcutName = strip_tags(isset($parsedBody['shortcutTitle']) ? $parsedBody['shortcutTitle'] : $queryParams['shortcutTitle']);
$shortcutGroupId = (int)(isset($parsedBody['shortcutGroup']) ? $parsedBody['shortcutGroup'] : $queryParams['shortcutGroup']);
// Users can only modify their own shortcuts (except admins)
$addUserWhere = !$backendUser->isAdmin() ? ' AND userid=' . (int)$backendUser->user['uid'] : '';
$fieldValues = array(
......@@ -662,11 +680,11 @@ class ShortcutToolbarItem implements ToolbarItemInterface {
$databaseConnection->exec_UPDATEquery('sys_be_shortcuts', 'uid=' . $shortcutId . $addUserWhere, $fieldValues);
$affectedRows = $databaseConnection->sql_affected_rows();
if ($affectedRows == 1) {
$ajaxObj->addContent('shortcut', $shortcutName);
$response->getBody()->write($shortcutName);
} else {
$ajaxObj->addContent('shortcut', 'failed');
$response->getBody()->write('failed');
}
$ajaxObj->setContentFormat('plain');
return $response->withHeader('Content-Type', 'html');
}
/**
......
......@@ -14,13 +14,14 @@ namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
use \TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\CommandUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -122,12 +123,16 @@ class SystemInformationToolbarItem implements ToolbarItemInterface {
/**
* Renders the menu for AJAX calls
*
* @param array $params
* @param AjaxRequestHandler $ajaxObj
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function renderAjax($params = array(), $ajaxObj) {
public function renderMenuAction(ServerRequestInterface $request, ResponseInterface $response) {
$this->collectInformation();
$ajaxObj->addContent('systemInformationMenu', $this->getDropDown());
$response->getBody()->write($this->getDropDown());
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
return $response;
}
/**
......
......@@ -866,16 +866,17 @@ class BackendController {
}
/**
* Returns the Module menu for the AJAX API
* Returns the Module menu for the AJAX request
*
* @param array $params
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxRequestHandler
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function getModuleMenuForReload($params, $ajaxRequestHandler) {
public function getModuleMenu(ServerRequestInterface $request, ResponseInterface $response) {
$content = $this->generateModuleMenu();
$ajaxRequestHandler->addContent('menu', $content);
$ajaxRequestHandler->setContentFormat('json');
$response->getBody()->write(json_encode(['menu' => $content]));
return $response;
}
/**
......
......@@ -14,11 +14,12 @@ namespace TYPO3\CMS\Backend\Controller;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Template\DocumentTemplate;
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\ClickMenu\ClickMenu;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
/**
* Script Class for the Context Sensitive Menu in TYPO3 (rendered in top frame, normally writing content dynamically to list frames).
......@@ -125,10 +126,11 @@ class ClickMenuController {
/**
* this is an intermediate clickmenu handler
*
* @param array $parameters
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxRequestHandler
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function printContentForAjaxRequest($parameters, AjaxRequestHandler $ajaxRequestHandler) {
public function getContextMenuAction(ServerRequestInterface $request, ResponseInterface $response) {
// XML has to be parsed, no parse errors allowed
@ini_set('display_errors', 0);
......@@ -154,8 +156,9 @@ class ClickMenuController {
// send the data
$ajaxContent = '<?xml version="1.0"?><t3ajax>' . $ajaxContent . '</t3ajax>';
$ajaxRequestHandler->addContent('ClickMenu', $ajaxContent);
$ajaxRequestHandler->setContentFormat('xml');
$response->getBody()->write($ajaxContent);
$response = $response->withHeader('Content-Type', 'text/xml; charset=utf-8');
return $response;
}
/**
......
......@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Backend\Controller\File;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Resource\DuplicationBehavior;
......@@ -183,8 +182,9 @@ class FileController {
BackendUtility::setUpdateSignal('updateFolderTree');
if ($this->redirect) {
$response = $response->withHeader('Location', GeneralUtility::locationHeaderUrl($this->redirect));
return $response->withStatus(303);
return $response
->withHeader('Location', GeneralUtility::locationHeaderUrl($this->redirect))
->withStatus(303);
} else {
// empty response
return $response;
......@@ -197,15 +197,16 @@ class FileController {
* but without calling the "finish" method, thus makes it simpler to deal with the
* actual return value
*
* @param array $params Always empty.
* @param AjaxRequestHandler $ajaxObj The AjaxRequestHandler object used to return content and set content types
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function processAjaxRequest(array $params, AjaxRequestHandler $ajaxObj) {
public function processAjaxRequest(ServerRequestInterface $request, ResponseInterface $response) {
$this->main();
$errors = $this->fileProcessor->getErrorMessages();
if (!empty($errors)) {
$ajaxObj->setError(implode(',', $errors));
$response->getBody()->write(implode(',', $errors));
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
} else {
$flatResult = array();
foreach ($this->fileData as $action => $results) {
......@@ -219,24 +220,25 @@ class FileController {
}
}
}
$ajaxObj->addContent('result', $flatResult);
$content = ['result' => $flatResult];
if ($this->redirect) {
$ajaxObj->addContent('redirect', $this->redirect);
$content['redirect'] = $this->redirect;
}
$ajaxObj->setContentFormat('json');
$response->getBody()->write(json_encode($content));
}
return $response;
}
/**
* Ajax entry point to check if a file exists in a folder
*
* @param array $params Always empty.
* @param AjaxRequestHandler $ajaxObj The AjaxRequestHandler object used to return content and set content types
* @return void
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function fileExistsAjaxRequest(array $params, AjaxRequestHandler $ajaxObj) {
$fileName = GeneralUtility::_GP('fileName');
$fileTarget = GeneralUtility::_GP('fileTarget');
public function fileExistsInFolderAction(ServerRequestInterface $request, ResponseInterface $response) {
$fileName = isset($request->getParsedBody()['fileName']) ? $request->getParsedBody()['fileName'] : $request->getQueryParams()['fileName'];
$fileTarget = isset($request->getParsedBody()['fileTarget']) ? $request->getParsedBody()['fileTarget'] : $request->getQueryParams()['fileTarget'];
/** @var \TYPO3\CMS\Core\Resource\ResourceFactory $fileFactory */
$fileFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\ResourceFactory::class);
......@@ -248,8 +250,8 @@ class FileController {
if ($fileTargetObject->hasFile($processedFileName)) {
$result = $this->flattenResultDataValue($fileTargetObject->getStorage()->getFileInFolder($processedFileName, $fileTargetObject));
}
$ajaxObj->addContent('result', $result);
$ajaxObj->setContentFormat('json');
$