4046a623b88a7836dcbabd3c0635af2d4dfa750b
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Http / AjaxRequestHandler.php
1 <?php
2 namespace TYPO3\CMS\Backend\Http;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
20 use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
21 use TYPO3\CMS\Core\Core\Bootstrap;
22 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
23 use TYPO3\CMS\Core\Http\RequestHandlerInterface;
24 use TYPO3\CMS\Core\Http\Response;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26
27 /**
28 * AJAX dispatcher
29 *
30 * Main entry point for AJAX calls in the TYPO3 Backend. Based on ?ajaxId of the outside application.
31 * Before doing the basic BE-related set up of this request (see the additional calls on $this->bootstrap inside
32 * handleRequest()), some AJAX-calls can be made without a valid user, which is determined here.
33 * See $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'] and the Core APIs on how to register an AJAX call in the TYPO3 Backend.
34 *
35 * Due to legacy reasons, the actual logic is in EXT:core/Http/AjaxRequestHandler which will eventually
36 * be moved into this class.
37 */
38 class AjaxRequestHandler implements RequestHandlerInterface
39 {
40 /**
41 * Instance of the current TYPO3 bootstrap
42 * @var Bootstrap
43 */
44 protected $bootstrap;
45
46 /**
47 * List of requests that don't need a valid BE user
48 * @var array
49 */
50 protected $publicAjaxIds = [
51 '/ajax/login',
52 '/ajax/logout',
53 '/ajax/login/refresh',
54 '/ajax/login/timedout',
55 '/ajax/rsa/publickey'
56 ];
57
58 /**
59 * Constructor handing over the bootstrap and the original request
60 *
61 * @param Bootstrap $bootstrap
62 */
63 public function __construct(Bootstrap $bootstrap)
64 {
65 $this->bootstrap = $bootstrap;
66 }
67
68 /**
69 * Handles any AJAX request in the TYPO3 Backend
70 *
71 * @param ServerRequestInterface $request
72 * @return null|\Psr\Http\Message\ResponseInterface
73 */
74 public function handleRequest(ServerRequestInterface $request)
75 {
76 // First get the ajaxID
77 $ajaxID = isset($request->getParsedBody()['ajaxID']) ? $request->getParsedBody()['ajaxID'] : $request->getQueryParams()['ajaxID'];
78 $request = $request->withAttribute('routePath', $ajaxID);
79 $proceedIfNoUserIsLoggedIn = $this->isLoggedInBackendUserRequired($ajaxID);
80 $this->boot($proceedIfNoUserIsLoggedIn);
81
82 try {
83 // Backend Routing - check if a valid route is there, and dispatch
84 return $this->dispatch($request);
85 } catch (ResourceNotFoundException $e) {
86 // no Route found, fallback to the traditional AJAX request
87 }
88 return $this->dispatchTraditionalAjaxRequest($request);
89 }
90
91 /**
92 * This request handler can handle any backend request having
93 * an ajaxID as parameter (see Application.php in EXT:backend)
94 *
95 * @param ServerRequestInterface $request
96 * @return bool If the request is an AJAX backend request, TRUE otherwise FALSE
97 */
98 public function canHandleRequest(ServerRequestInterface $request)
99 {
100 return $request->getAttribute('isAjaxRequest', false);
101 }
102
103 /**
104 * Returns the priority - how eager the handler is to actually handle the request.
105 *
106 * @return int The priority of the request handler.
107 */
108 public function getPriority()
109 {
110 return 80;
111 }
112
113 /**
114 * Check if the user is required for the request
115 * If we're trying to do an ajax login, don't require a user
116 *
117 * @param string $ajaxId the Ajax ID to check against
118 * @return bool whether the request can proceed without a login required
119 */
120 protected function isLoggedInBackendUserRequired($ajaxId)
121 {
122 return in_array($ajaxId, $this->publicAjaxIds, true);
123 }
124
125 /**
126 * Start the Backend bootstrap part
127 *
128 * @param bool $proceedIfNoUserIsLoggedIn a flag if a backend user is required
129 */
130 protected function boot($proceedIfNoUserIsLoggedIn)
131 {
132 $this->bootstrap
133 ->checkLockedBackendAndRedirectOrDie($proceedIfNoUserIsLoggedIn)
134 ->checkBackendIpOrDie()
135 ->checkSslBackendAndRedirectIfNeeded()
136 ->initializeBackendRouter()
137 ->loadBaseTca()
138 ->loadExtTables()
139 ->initializeBackendUser()
140 ->initializeBackendAuthentication($proceedIfNoUserIsLoggedIn)
141 ->initializeLanguageObject()
142 ->initializeBackendTemplate()
143 ->endOutputBufferingAndCleanPreviousOutput()
144 ->initializeOutputCompression()
145 ->sendHttpHeaders();
146 }
147
148 /**
149 * Creates a response object with JSON headers automatically, and then dispatches to the correct route
150 *
151 * @param ServerRequestInterface $request
152 * @return ResponseInterface $response
153 * @throws ResourceNotFoundException if no valid route was found
154 * @throws InvalidRequestTokenException if the request could not be verified
155 */
156 protected function dispatch(ServerRequestInterface $request)
157 {
158 /** @var Response $response */
159 $response = GeneralUtility::makeInstance(Response::class, 'php://temp', 200, [
160 'Content-Type' => 'application/json; charset=utf-8',
161 'X-JSON' => 'true'
162 ]);
163
164 /** @var RouteDispatcher $dispatcher */
165 $dispatcher = GeneralUtility::makeInstance(RouteDispatcher::class);
166 return $dispatcher->dispatch($request, $response);
167 }
168
169 /**
170 * Calls the ajax callback method registered in TYPO3_CONF_VARS[BE][AJAX]
171 *
172 * @param ServerRequestInterface $request
173 * @return ResponseInterface
174 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
175 */
176 protected function dispatchTraditionalAjaxRequest($request)
177 {
178 GeneralUtility::deprecationLog('Using the traditional way for AJAX requests via $TYPO3_CONF_VARS[BE][AJAX] is discouraged. Use the Backend Routes logic instead.');
179 $ajaxID = $request->getAttribute('routePath');
180 // Finding the script path from the registry
181 $ajaxRegistryEntry = isset($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID]) ? $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID] : null;
182 $ajaxScript = null;
183 $csrfTokenCheck = false;
184 if ($ajaxRegistryEntry !== null && is_array($ajaxRegistryEntry) && isset($ajaxRegistryEntry['callbackMethod'])) {
185 $ajaxScript = $ajaxRegistryEntry['callbackMethod'];
186 $csrfTokenCheck = $ajaxRegistryEntry['csrfTokenCheck'];
187 }
188
189 // Instantiating the AJAX object
190 /** @var \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj */
191 $ajaxObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\AjaxRequestHandler::class, $ajaxID);
192 $ajaxParams = ['request' => $request];
193
194 // Evaluating the arguments and calling the AJAX method/function
195 if (empty($ajaxID)) {
196 $ajaxObj->setError('No valid ajaxID parameter given.');
197 } elseif (empty($ajaxScript)) {
198 $ajaxObj->setError('No backend function registered for ajaxID "' . $ajaxID . '".');
199 } elseif ($csrfTokenCheck && !$this->isValidRequest($request)) {
200 $ajaxObj->setError('Invalid CSRF token detected for ajaxID "' . $ajaxID . '", reload the backend of TYPO3');
201 } else {
202 $success = GeneralUtility::callUserFunction($ajaxScript, $ajaxParams, $ajaxObj, '', 1);
203 if ($success === false) {
204 $ajaxObj->setError('Registered backend function for ajaxID "' . $ajaxID . '" was not found.');
205 }
206 }
207
208 // Outputting the content (and setting the X-JSON-Header)
209 return $ajaxObj->render();
210 }
211
212 /**
213 * Wrapper method for static form protection utility
214 *
215 * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
216 */
217 protected function getFormProtection()
218 {
219 return FormProtectionFactory::get();
220 }
221
222 /**
223 * Checks if the request token is valid. This is checked to see if the route is really
224 * created by the same instance. Should be called for all routes in the backend except
225 * for the ones that don't require a login.
226 *
227 * @param ServerRequestInterface $request
228 * @return bool
229 * @see \TYPO3\CMS\Backend\Routing\UriBuilder where the token is generated.
230 */
231 protected function isValidRequest(ServerRequestInterface $request)
232 {
233 $token = (string)(isset($request->getParsedBody()['ajaxToken']) ? $request->getParsedBody()['ajaxToken'] : $request->getQueryParams()['ajaxToken']);
234 return $this->getFormProtection()->validateToken($token, 'ajaxCall', $request->getAttribute('routePath'));
235 }
236 }