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