3a08d59ddef9c3b708d5c054a861d8fe8b5dd999
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / MVC / Controller / ActionController.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
6 * All rights reserved
7 *
8 * This class is a backport of the corresponding class of FLOW3.
9 * All credits go to the v5 team.
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27
28 /**
29 * A multi action controller. This is by far the most common base class for Controllers.
30 *
31 * @package Extbase
32 * @subpackage MVC\Controller
33 * @version $ID:$
34 * @api
35 */
36 class Tx_Extbase_MVC_Controller_ActionController extends Tx_Extbase_MVC_Controller_AbstractController {
37
38 /**
39 * @var Tx_Extbase_Reflection_Service
40 */
41 protected $reflectionService;
42
43 /**
44 * @var Tx_Extbase_Service_CacheService
45 */
46 protected $cacheService;
47
48 /**
49 * The current view, as resolved by resolveView()
50 *
51 * @var Tx_Extbase_MVC_View_ViewInterface
52 * @api
53 */
54 protected $view = NULL;
55
56 /**
57 * Pattern after which the view object name is built if no Fluid template
58 * is found.
59 * @var string
60 * @api
61 */
62 protected $viewObjectNamePattern = 'Tx_@extension_View_@controller_@action@format';
63
64 /**
65 * A list of formats and object names of the views which should render them.
66 *
67 * Example:
68 *
69 * array('html' => 'Tx_MyExtension_View_MyHtmlView', 'json' => 'F3...
70 *
71 * @var array
72 */
73 protected $viewFormatToObjectNameMap = array();
74
75 /**
76 * The default view object to use if none of the resolved views can render
77 * a response for the current request.
78 *
79 * @var string
80 * @api
81 */
82 protected $defaultViewObjectName = 'Tx_Fluid_View_TemplateView';
83
84 /**
85 * Name of the action method
86 * @var string
87 * @api
88 */
89 protected $actionMethodName = 'indexAction';
90
91 /**
92 * Name of the special error action method which is called in case of errors
93 * @var string
94 * @api
95 */
96 protected $errorMethodName = 'errorAction';
97
98 /**
99 * @param Tx_Extbase_Reflection_Service $reflectionService
100 * @return void
101 */
102 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
103 $this->reflectionService = $reflectionService;
104 }
105
106 /**
107 * @param Tx_Extbase_Service_CacheService $cacheService
108 * @return void
109 */
110 public function injectCacheService(Tx_Extbase_Service_CacheService $cacheService) {
111 $this->cacheService = $cacheService;
112 }
113
114 /**
115 * Checks if the current request type is supported by the controller.
116 *
117 * If your controller only supports certain request types, either
118 * replace / modify the supporteRequestTypes property or override this
119 * method.
120 *
121 * @param Tx_Extbase_MVC_Request $request The current request
122 * @return boolean TRUE if this request type is supported, otherwise FALSE
123 */
124 public function canProcessRequest(Tx_Extbase_MVC_RequestInterface $request) {
125 return parent::canProcessRequest($request);
126
127 }
128
129 /**
130 * Handles a request. The result output is returned by altering the given response.
131 *
132 * @param Tx_Extbase_MVC_Request $request The request object
133 * @param Tx_Extbase_MVC_Response $response The response, modified by this handler
134 * @return void
135 */
136 public function processRequest(Tx_Extbase_MVC_RequestInterface $request, Tx_Extbase_MVC_ResponseInterface $response) {
137 if (!$this->canProcessRequest($request)) {
138 throw new Tx_Extbase_MVC_Exception_UnsupportedRequestType(get_class($this) . ' does not support requests of type "' . get_class($request) . '". Supported types are: ' . implode(' ', $this->supportedRequestTypes) , 1187701131);
139 }
140
141 $response->setRequest($request);
142 $this->request = $request;
143 $this->request->setDispatched(TRUE);
144 $this->response = $response;
145
146 $this->uriBuilder = $this->objectManager->create('Tx_Extbase_MVC_Web_Routing_UriBuilder');
147 $this->uriBuilder->setRequest($request);
148
149 $this->actionMethodName = $this->resolveActionMethodName();
150
151 $this->initializeActionMethodArguments();
152 $this->initializeActionMethodValidators();
153
154 $this->initializeAction();
155 $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
156 if (method_exists($this, $actionInitializationMethodName)) {
157 call_user_func(array($this, $actionInitializationMethodName));
158 }
159
160 $this->mapRequestArgumentsToControllerArguments();
161 $this->checkRequestHash();
162 $this->controllerContext = $this->buildControllerContext();
163 $this->view = $this->resolveView();
164 if ($this->view !== NULL) {
165 $this->initializeView($this->view);
166 }
167 $this->callActionMethod();
168 }
169
170 /**
171 * Implementation of the arguments initilization in the action controller:
172 * Automatically registers arguments of the current action
173 *
174 * Don't override this method - use initializeAction() instead.
175 *
176 * @return void
177 * @see initializeArguments()
178 */
179 protected function initializeActionMethodArguments() {
180 $methodParameters = $this->reflectionService->getMethodParameters(get_class($this), $this->actionMethodName);
181
182 foreach ($methodParameters as $parameterName => $parameterInfo) {
183 $dataType = NULL;
184 if (isset($parameterInfo['type'])) {
185 $dataType = $parameterInfo['type'];
186 } elseif ($parameterInfo['array']) {
187 $dataType = 'array';
188 }
189 if ($dataType === NULL) throw new Tx_Extbase_MVC_Exception_InvalidArgumentType('The argument type for parameter $' . $parameterName . ' of method ' . get_class($this) . '->' . $this->actionMethodName . '() could not be detected.' , 1253175643);
190
191 $defaultValue = (isset($parameterInfo['defaultValue']) ? $parameterInfo['defaultValue'] : NULL);
192
193 $this->arguments->addNewArgument($parameterName, $dataType, ($parameterInfo['optional'] === FALSE), $defaultValue);
194 }
195 }
196
197 /**
198 * Adds the needed valiators to the Arguments:
199 * - Validators checking the data type from the @param annotation
200 * - Custom validators specified with @validate.
201 *
202 * In case @dontvalidate is NOT set for an argument, the following two
203 * validators are also added:
204 * - Model-based validators (@validate annotations in the model)
205 * - Custom model validator classes
206 *
207 * @return void
208 */
209 protected function initializeActionMethodValidators() {
210 // TODO: still needs to be modified
211 $parameterValidators = $this->validatorResolver->buildMethodArgumentsValidatorConjunctions(get_class($this), $this->actionMethodName);
212
213 $dontValidateAnnotations = array();
214
215 if (!$this->configurationManager->isFeatureEnabled('rewrittenPropertyMapper')) {
216 // If the rewritten property mapper is *enabled*, we do not support @dontvalidate annotation, thus $dontValidateAnnotations stays empty.
217 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
218 if (isset($methodTagsValues['dontvalidate'])) {
219 $dontValidateAnnotations = $methodTagsValues['dontvalidate'];
220 }
221 }
222
223 foreach ($this->arguments as $argument) {
224 $validator = $parameterValidators[$argument->getName()];
225
226 if (array_search('$' . $argument->getName(), $dontValidateAnnotations) === FALSE) {
227 $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
228 if ($baseValidatorConjunction !== NULL) {
229 $validator->addValidator($baseValidatorConjunction);
230 }
231 }
232 $argument->setValidator($validator);
233 }
234 }
235
236 /**
237 * Resolves and checks the current action method name
238 *
239 * @return string Method name of the current action
240 * @throws Tx_Extbase_MVC_Exception_NoSuchAction if the action specified in the request object does not exist (and if there's no default action either).
241 */
242 protected function resolveActionMethodName() {
243 $actionMethodName = $this->request->getControllerActionName() . 'Action';
244 if (!method_exists($this, $actionMethodName)) throw new Tx_Extbase_MVC_Exception_NoSuchAction('An action "' . $actionMethodName . '" does not exist in controller "' . get_class($this) . '".', 1186669086);
245 return $actionMethodName;
246 }
247
248 /**
249 * Calls the specified action method and passes the arguments.
250 *
251 * If the action returns a string, it is appended to the content in the
252 * response object. If the action doesn't return anything and a valid
253 * view exists, the view is rendered automatically.
254 *
255 * @param string $actionMethodName Name of the action method to call
256 * @return void
257 * @api
258 */
259 protected function callActionMethod() {
260 if ($this->configurationManager->isFeatureEnabled('rewrittenPropertyMapper')) {
261 // enabled since Extbase 1.4.0.
262 $preparedArguments = array();
263 foreach ($this->arguments as $argument) {
264 $preparedArguments[] = $argument->getValue();
265 }
266
267 $validationResult = $this->arguments->getValidationResults();
268
269 if (!$validationResult->hasErrors()) {
270 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
271 } else {
272 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
273 $ignoreValidationAnnotations = array();
274 if (isset($methodTagsValues['ignorevalidation'])) {
275 $ignoreValidationAnnotations = $methodTagsValues['ignorevalidation'];
276 }
277
278 // if there exists more errors than in ignoreValidationAnnotations_=> call error method
279 // else => call action method
280 $shouldCallActionMethod = TRUE;
281 foreach ($validationResult->getSubResults() as $argumentName => $subValidationResult) {
282 if (!$subValidationResult->hasErrors()) continue;
283
284 if (array_search('$' . $argumentName, $ignoreValidationAnnotations) !== FALSE) continue;
285
286 $shouldCallActionMethod = FALSE;
287 }
288
289 if ($shouldCallActionMethod) {
290 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
291 } else {
292 $actionResult = call_user_func(array($this, $this->errorMethodName));
293 }
294 }
295 } else {
296 // @deprecated since Extbase 1.4.0, will be removed with Extbase 6.0
297 $argumentsAreValid = TRUE;
298 $preparedArguments = array();
299 foreach ($this->arguments as $argument) {
300 $preparedArguments[] = $argument->getValue();
301 }
302
303 if ($this->argumentsMappingResults->hasErrors()) {
304 $actionResult = call_user_func(array($this, $this->errorMethodName));
305 } else {
306 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
307 }
308 }
309
310 if ($actionResult === NULL && $this->view instanceof Tx_Extbase_MVC_View_ViewInterface) {
311 $this->response->appendContent($this->view->render());
312 } elseif (is_string($actionResult) && strlen($actionResult) > 0) {
313 $this->response->appendContent($actionResult);
314 } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
315 $this->response->appendContent((string)$actionResult);
316 }
317 }
318
319 /**
320 * Prepares a view for the current action and stores it in $this->view.
321 * By default, this method tries to locate a view with a name matching
322 * the current action.
323 *
324 * @return void
325 * @api
326 */
327 protected function resolveView() {
328 $viewObjectName = $this->resolveViewObjectName();
329 if ($viewObjectName !== FALSE) {
330 $view = $this->objectManager->create($viewObjectName);
331 $this->setViewConfiguration($view);
332 if ($view->canRender($this->controllerContext) === FALSE) {
333 unset($view);
334 }
335 }
336 if (!isset($view) && $this->defaultViewObjectName != '') {
337 $view = $this->objectManager->create($this->defaultViewObjectName);
338 $this->setViewConfiguration($view);
339 if ($view->canRender($this->controllerContext) === FALSE) {
340 unset($view);
341 }
342 }
343 if (!isset($view)) {
344 $view = $this->objectManager->create('Tx_Extbase_MVC_View_NotFoundView');
345 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "' . $this->request->getControllerActionName() . '"');
346 }
347 $view->setControllerContext($this->controllerContext);
348
349 if (method_exists($view, 'injectSettings')) {
350 $view->injectSettings($this->settings);
351 }
352 $view->initializeView(); // In FLOW3, solved through Object Lifecycle methods, we need to call it explicitely
353 $view->assign('settings', $this->settings); // same with settings injection.
354 return $view;
355 }
356
357 /**
358 * @param Tx_Extbase_MVC_View_ViewInterface $view
359 * @return void
360 */
361 protected function setViewConfiguration(Tx_Extbase_MVC_View_ViewInterface $view) {
362 // Template Path Override
363 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(Tx_Extbase_Configuration_ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
364 if (isset($extbaseFrameworkConfiguration['view']['templateRootPath'])
365 && strlen($extbaseFrameworkConfiguration['view']['templateRootPath']) > 0
366 && method_exists($view, 'setTemplateRootPath')) {
367 $view->setTemplateRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['templateRootPath']));
368 }
369 if (isset($extbaseFrameworkConfiguration['view']['layoutRootPath'])
370 && strlen($extbaseFrameworkConfiguration['view']['layoutRootPath']) > 0
371 && method_exists($view, 'setLayoutRootPath')) {
372 $view->setLayoutRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['layoutRootPath']));
373 }
374 if (isset($extbaseFrameworkConfiguration['view']['partialRootPath'])
375 && strlen($extbaseFrameworkConfiguration['view']['partialRootPath']) > 0
376 && method_exists($view, 'setPartialRootPath')) {
377 $view->setPartialRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['partialRootPath']));
378 }
379 }
380
381 /**
382 * Determines the fully qualified view object name.
383 *
384 * @return mixed The fully qualified view object name or FALSE if no matching view could be found.
385 * @api
386 */
387 protected function resolveViewObjectName() {
388 $possibleViewName = $this->viewObjectNamePattern;
389 $extensionName = $this->request->getControllerExtensionName();
390 $possibleViewName = str_replace('@extension', $extensionName, $possibleViewName);
391 $possibleViewName = str_replace('@controller', $this->request->getControllerName(), $possibleViewName);
392 $possibleViewName = str_replace('@action', ucfirst($this->request->getControllerActionName()), $possibleViewName);
393 $format = $this->request->getFormat();
394
395 $viewObjectName = str_replace('@format', ucfirst($this->request->getFormat()), $possibleViewName);
396 if (class_exists($viewObjectName) === FALSE) {
397 $viewObjectName = str_replace('@format', '', $possibleViewName);
398 }
399 if (class_exists($viewObjectName) === FALSE && isset($this->viewFormatToObjectNameMap[$format])) {
400 $viewObjectName = $this->viewFormatToObjectNameMap[$format];
401 }
402 return class_exists($viewObjectName) ? $viewObjectName : FALSE;
403 }
404
405 /**
406 * Initializes the view before invoking an action method.
407 *
408 * Override this method to solve assign variables common for all actions
409 * or prepare the view in another way before the action is called.
410 *
411 * @param Tx_Extbase_MVC_View_ViewInterface $view The view to be initialized
412 * @return void
413 * @api
414 */
415 protected function initializeView(Tx_Extbase_MVC_View_ViewInterface $view) {
416 }
417
418 /**
419 * Initializes the controller before invoking an action method.
420 *
421 * Override this method to solve tasks which all actions have in
422 * common.
423 *
424 * @return void
425 * @api
426 */
427 protected function initializeAction() {
428 }
429
430 /**
431 * A special action which is called if the originally intended action could
432 * not be called, for example if the arguments were not valid.
433 *
434 * The default implementation sets a flash message, request errors and forwards back
435 * to the originating action. This is suitable for most actions dealing with form input.
436 *
437 * We clear the page cache by default on an error as well, as we need to make sure the
438 * data is re-evaluated when the user changes something.
439 *
440 * @return string
441 * @api
442 */
443 protected function errorAction() {
444 $this->clearCacheOnError();
445
446 if ($this->configurationManager->isFeatureEnabled('rewrittenPropertyMapper')) {
447 $errorFlashMessage = $this->getErrorFlashMessage();
448 if ($errorFlashMessage !== FALSE) {
449 $this->flashMessageContainer->add($errorFlashMessage, '', t3lib_FlashMessage::ERROR);
450 }
451
452 $referringRequest = $this->request->getReferringRequest();
453 if ($referringRequest !== NULL) {
454 $originalRequest = clone $this->request;
455 $this->request->setOriginalRequest($originalRequest);
456 $this->request->setOriginalRequestMappingResults($this->arguments->getValidationResults());
457
458 $this->forward($referringRequest->getControllerActionName(), $referringRequest->getControllerName(), $referringRequest->getControllerExtensionName(), $referringRequest->getArguments());
459 }
460
461 $message = 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '().' . PHP_EOL;
462 foreach ($this->arguments->getValidationResults()->getFlattenedErrors() as $propertyPath => $errors) {
463 foreach ($errors as $error) {
464 $message .= 'Error for ' . $propertyPath . ': ' . $error->getMessage() . PHP_EOL;
465 }
466 }
467
468 return $message;
469 } else {
470 // @deprecated since Extbase 1.4.0, will be removed in Extbase 6.0
471 $this->request->setErrors($this->argumentsMappingResults->getErrors());
472
473 $errorFlashMessage = $this->getErrorFlashMessage();
474 if ($errorFlashMessage !== FALSE) {
475 $this->flashMessageContainer->add($errorFlashMessage, '', t3lib_FlashMessage::ERROR);
476 }
477
478 $referrer = $this->request->getInternalArgument('__referrer');
479 if ($referrer !== NULL) {
480 $this->forward($referrer['actionName'], $referrer['controllerName'], $referrer['extensionName'], $this->request->getArguments());
481 }
482
483 $message = 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '().' . PHP_EOL;
484 foreach ($this->argumentsMappingResults->getErrors() as $error) {
485 $message .= 'Error: ' . $error->getMessage() . PHP_EOL;
486 }
487 foreach ($this->argumentsMappingResults->getWarnings() as $warning) {
488 $message .= 'Warning: ' . $warning->getMessage() . PHP_EOL;
489 }
490 return $message;
491 }
492 }
493
494 /**
495 * A template method for displaying custom error flash messages, or to
496 * display no flash message at all on errors. Override this to customize
497 * the flash message in your action controller.
498 *
499 * @return string|boolean The flash message or FALSE if no flash message should be set
500 * @api
501 */
502 protected function getErrorFlashMessage() {
503 return 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '()';
504 }
505
506 /**
507 * Checks the request hash (HMAC), if arguments have been touched by the property mapper.
508 *
509 * In case the @dontverifyrequesthash-Annotation has been set, this suppresses the exception.
510 *
511 * @return void
512 * @throws Tx_Extbase_MVC_Exception_InvalidOrNoRequestHash In case request hash checking failed
513 * @author Sebastian Kurf├╝rst <sebastian@typo3.org>
514 * @deprecated since Extbase 1.4.0, will be removed in Extbase 6.0
515 */
516 protected function checkRequestHash() {
517 if ($this->configurationManager->isFeatureEnabled('rewrittenPropertyMapper')) {
518 // If the new property mapper is enabled, the request hash is not needed anymore.
519 return;
520 }
521 if (!($this->request instanceof Tx_Extbase_MVC_Web_Request)) return; // We only want to check it for now for web requests.
522 if ($this->request->isHmacVerified()) return; // all good
523
524 $verificationNeeded = FALSE;
525 foreach ($this->arguments as $argument) {
526 if ($argument->getOrigin() == Tx_Extbase_MVC_Controller_Argument::ORIGIN_NEWLY_CREATED
527 || $argument->getOrigin() == Tx_Extbase_MVC_Controller_Argument::ORIGIN_PERSISTENCE_AND_MODIFIED) {
528 $verificationNeeded = TRUE;
529 }
530 }
531 if ($verificationNeeded) {
532 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
533 if (!isset($methodTagsValues['dontverifyrequesthash'])) {
534 throw new Tx_Extbase_MVC_Exception_InvalidOrNoRequestHash('Request hash (HMAC) checking failed. The parameter __hmac was invalid or not set, and objects were modified.', 1255082824);
535 }
536 }
537 }
538
539 /**
540 * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
541 * Better would be just do delete the cache for the error action, but that is not possible right now.
542 *
543 * @return void
544 */
545 protected function clearCacheOnError() {
546 $extbaseSettings = $this->configurationManager->getConfiguration(Tx_Extbase_Configuration_ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
547 if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
548 if (isset($GLOBALS['TSFE'])) {
549 $pageUid = $GLOBALS['TSFE']->id;
550 $this->cacheService->clearPageCache(array($pageUid));
551 }
552 }
553 }
554
555 }
556 ?>