b5930c307b462f5ffcc777083630e5ef56a94c5b
[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 * The current view, as resolved by resolveView()
45 *
46 * @var Tx_Extbase_MVC_View_ViewInterface
47 * @api
48 */
49 protected $view = NULL;
50
51 /**
52 * Pattern after which the view object name is built if no Fluid template
53 * is found.
54 * @var string
55 * @api
56 */
57 protected $viewObjectNamePattern = 'Tx_@extension_View_@controller_@action@format';
58
59 /**
60 * A list of formats and object names of the views which should render them.
61 *
62 * Example:
63 *
64 * array('html' => 'Tx_MyExtension_View_MyHtmlView', 'json' => 'F3...
65 *
66 * @var array
67 */
68 protected $viewFormatToObjectNameMap = array();
69
70 /**
71 * The default view object to use if none of the resolved views can render
72 * a response for the current request.
73 *
74 * @var string
75 * @api
76 */
77 protected $defaultViewObjectName = 'Tx_Fluid_View_TemplateView';
78
79 /**
80 * Name of the action method
81 * @var string
82 * @api
83 */
84 protected $actionMethodName = 'indexAction';
85
86 /**
87 * Name of the special error action method which is called in case of errors
88 * @var string
89 * @api
90 */
91 protected $errorMethodName = 'errorAction';
92
93 /**
94 * Injects the reflection service
95 *
96 * @param Tx_Extbase_Reflection_Service $reflectionService
97 * @return void
98 */
99 public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
100 $this->reflectionService = $reflectionService;
101 }
102
103 /**
104 * Checks if the current request type is supported by the controller.
105 *
106 * If your controller only supports certain request types, either
107 * replace / modify the supporteRequestTypes property or override this
108 * method.
109 *
110 * @param Tx_Extbase_MVC_Request $request The current request
111 * @return boolean TRUE if this request type is supported, otherwise FALSE
112 */
113 public function canProcessRequest(Tx_Extbase_MVC_RequestInterface $request) {
114 return parent::canProcessRequest($request);
115
116 }
117
118 /**
119 * Handles a request. The result output is returned by altering the given response.
120 *
121 * @param Tx_Extbase_MVC_Request $request The request object
122 * @param Tx_Extbase_MVC_Response $response The response, modified by this handler
123 * @return void
124 */
125 public function processRequest(Tx_Extbase_MVC_RequestInterface $request, Tx_Extbase_MVC_ResponseInterface $response) {
126 if (!$this->canProcessRequest($request)) 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);
127
128 $this->request = $request;
129 $this->request->setDispatched(TRUE);
130 $this->response = $response;
131
132 $this->uriBuilder = $this->objectManager->create('Tx_Extbase_MVC_Web_Routing_UriBuilder');
133 $this->uriBuilder->setRequest($request);
134
135 $this->actionMethodName = $this->resolveActionMethodName();
136
137 $this->initializeActionMethodArguments();
138 $this->initializeActionMethodValidators();
139
140 $this->initializeAction();
141 $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
142 if (method_exists($this, $actionInitializationMethodName)) {
143 call_user_func(array($this, $actionInitializationMethodName));
144 }
145
146 $this->mapRequestArgumentsToControllerArguments();
147 $this->checkRequestHash();
148 $this->controllerContext = $this->buildControllerContext();
149 $this->view = $this->resolveView();
150 if ($this->view !== NULL) $this->initializeView($this->view);
151 $this->callActionMethod();
152 }
153
154 /**
155 * Implementation of the arguments initilization in the action controller:
156 * Automatically registers arguments of the current action
157 *
158 * Don't override this method - use initializeAction() instead.
159 *
160 * @return void
161 * @see initializeArguments()
162 */
163 protected function initializeActionMethodArguments() {
164 $methodParameters = $this->reflectionService->getMethodParameters(get_class($this), $this->actionMethodName);
165
166 foreach ($methodParameters as $parameterName => $parameterInfo) {
167 $dataType = NULL;
168 if (isset($parameterInfo['type'])) {
169 $dataType = $parameterInfo['type'];
170 } elseif ($parameterInfo['array']) {
171 $dataType = 'array';
172 }
173 if ($dataType === NULL) throw new Tx_Extbase_MVC_Exception_InvalidArgumentType('The argument type for parameter "' . $parameterName . '" could not be detected.', 1253175643);
174
175 $defaultValue = (isset($parameterInfo['defaultValue']) ? $parameterInfo['defaultValue'] : NULL);
176
177 $this->arguments->addNewArgument($parameterName, $dataType, ($parameterInfo['optional'] === FALSE), $defaultValue);
178 }
179 }
180
181 /**
182 * Adds the needed valiators to the Arguments:
183 * - Validators checking the data type from the @param annotation
184 * - Custom validators specified with @validate.
185 *
186 * In case @dontvalidate is NOT set for an argument, the following two
187 * validators are also added:
188 * - Model-based validators (@validate annotations in the model)
189 * - Custom model validator classes
190 *
191 * @return void
192 */
193 protected function initializeActionMethodValidators() {
194 $parameterValidators = $this->validatorResolver->buildMethodArgumentsValidatorConjunctions(get_class($this), $this->actionMethodName);
195
196 $dontValidateAnnotations = array();
197 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
198 if (isset($methodTagsValues['dontvalidate'])) {
199 $dontValidateAnnotations = $methodTagsValues['dontvalidate'];
200 }
201
202 foreach ($this->arguments as $argument) {
203 $validator = $parameterValidators[$argument->getName()];
204
205 if (array_search('$' . $argument->getName(), $dontValidateAnnotations) === FALSE) {
206 $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
207 if ($baseValidatorConjunction !== NULL) {
208 $validator->addValidator($baseValidatorConjunction);
209 }
210 }
211 $argument->setValidator($validator);
212 }
213 }
214
215 /**
216 * Resolves and checks the current action method name
217 *
218 * @return string Method name of the current action
219 * @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).
220 */
221 protected function resolveActionMethodName() {
222 $actionMethodName = $this->request->getControllerActionName() . 'Action';
223 if (!method_exists($this, $actionMethodName)) throw new Tx_Extbase_MVC_Exception_NoSuchAction('An action "' . $actionMethodName . '" does not exist in controller "' . get_class($this) . '".', 1186669086);
224 return $actionMethodName;
225 }
226
227 /**
228 * Calls the specified action method and passes the arguments.
229 *
230 * If the action returns a string, it is appended to the content in the
231 * response object. If the action doesn't return anything and a valid
232 * view exists, the view is rendered automatically.
233 *
234 * @param string $actionMethodName Name of the action method to call
235 * @return void
236 * @api
237 */
238 protected function callActionMethod() {
239 $argumentsAreValid = TRUE;
240 $preparedArguments = array();
241 foreach ($this->arguments as $argument) {
242 $preparedArguments[] = $argument->getValue();
243 }
244
245 if ($this->argumentsMappingResults->hasErrors()) {
246 $actionResult = call_user_func(array($this, $this->errorMethodName));
247 } else {
248 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
249 }
250 if ($actionResult === NULL && $this->view instanceof Tx_Extbase_MVC_View_ViewInterface) {
251 $this->response->appendContent($this->view->render());
252 } elseif (is_string($actionResult) && strlen($actionResult) > 0) {
253 $this->response->appendContent($actionResult);
254 }
255 }
256
257 /**
258 * Prepares a view for the current action and stores it in $this->view.
259 * By default, this method tries to locate a view with a name matching
260 * the current action.
261 *
262 * @return void
263 * @api
264 */
265 protected function resolveView() {
266 $viewObjectName = $this->resolveViewObjectName();
267 if ($viewObjectName !== FALSE) {
268 $view = $this->objectManager->create($viewObjectName);
269 if ($view->canRender($this->controllerContext) === FALSE) {
270 unset($view);
271 }
272 }
273 if (!isset($view) && $this->defaultViewObjectName != '') {
274 $view = $this->objectManager->create($this->defaultViewObjectName);
275 if ($view->canRender($this->controllerContext) === FALSE) {
276 unset($view);
277 }
278 }
279 if (!isset($view)) {
280 $view = $this->objectManager->create('Tx_Extbase_MVC_View_NotFoundView');
281 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "' . $this->request->getControllerActionName() . '"');
282 }
283 $view->setControllerContext($this->controllerContext);
284
285 $this->setViewConfiguration($view);
286
287 if (method_exists($view, 'injectSettings')) {
288 $view->injectSettings($this->settings);
289 }
290 $view->initializeView(); // In FLOW3, solved through Object Lifecycle methods, we need to call it explicitely
291 $view->assign('settings', $this->settings); // same with settings injection.
292 return $view;
293 }
294
295 /**
296 * @param Tx_Extbase_MVC_View_ViewInterface $view
297 * @return void
298 */
299 protected function setViewConfiguration(Tx_Extbase_MVC_View_ViewInterface $view) {
300 // Template Path Override
301 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(Tx_Extbase_Configuration_ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
302 if (isset($extbaseFrameworkConfiguration['view']['templateRootPath'])
303 && strlen($extbaseFrameworkConfiguration['view']['templateRootPath']) > 0
304 && method_exists($view, 'setTemplateRootPath')) {
305 $view->setTemplateRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['templateRootPath']));
306 }
307 if (isset($extbaseFrameworkConfiguration['view']['layoutRootPath'])
308 && strlen($extbaseFrameworkConfiguration['view']['layoutRootPath']) > 0
309 && method_exists($view, 'setLayoutRootPath')) {
310 $view->setLayoutRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['layoutRootPath']));
311 }
312 if (isset($extbaseFrameworkConfiguration['view']['partialRootPath'])
313 && strlen($extbaseFrameworkConfiguration['view']['partialRootPath']) > 0
314 && method_exists($view, 'setPartialRootPath')) {
315 $view->setPartialRootPath(t3lib_div::getFileAbsFileName($extbaseFrameworkConfiguration['view']['partialRootPath']));
316 }
317 }
318
319 /**
320 * Determines the fully qualified view object name.
321 *
322 * @return mixed The fully qualified view object name or FALSE if no matching view could be found.
323 * @api
324 */
325 protected function resolveViewObjectName() {
326 $possibleViewName = $this->viewObjectNamePattern;
327 $extensionName = $this->request->getControllerExtensionName();
328 $possibleViewName = str_replace('@extension', $extensionName, $possibleViewName);
329 $possibleViewName = str_replace('@controller', $this->request->getControllerName(), $possibleViewName);
330 $possibleViewName = str_replace('@action', ucfirst($this->request->getControllerActionName()), $possibleViewName);
331 $format = $this->request->getFormat();
332
333 $viewObjectName = str_replace('@format', ucfirst($this->request->getFormat()), $possibleViewName);
334 if (class_exists($viewObjectName) === FALSE) {
335 $viewObjectName = str_replace('@format', '', $possibleViewName);
336 }
337 if (class_exists($viewObjectName) === FALSE && isset($this->viewFormatToObjectNameMap[$format])) {
338 $viewObjectName = $this->viewFormatToObjectNameMap[$format];
339 }
340 return class_exists($viewObjectName) ? $viewObjectName : FALSE;
341 }
342
343 /**
344 * Initializes the view before invoking an action method.
345 *
346 * Override this method to solve assign variables common for all actions
347 * or prepare the view in another way before the action is called.
348 *
349 * @param Tx_Extbase_View_ViewInterface $view The view to be initialized
350 * @return void
351 * @api
352 */
353 protected function initializeView(Tx_Extbase_MVC_View_ViewInterface $view) {
354 }
355
356 /**
357 * Initializes the controller before invoking an action method.
358 *
359 * Override this method to solve tasks which all actions have in
360 * common.
361 *
362 * @return void
363 * @api
364 */
365 protected function initializeAction() {
366 }
367
368 /**
369 * A special action which is called if the originally intended action could
370 * not be called, for example if the arguments were not valid.
371 *
372 * The default implementation sets a flash message, request errors and forwards back
373 * to the originating action. This is suitable for most actions dealing with form input.
374 *
375 * We clear the page cache by default on an error as well, as we need to make sure the
376 * data is re-evaluated when the user changes something.
377 *
378 * @return string
379 * @api
380 */
381 protected function errorAction() {
382 $this->request->setErrors($this->argumentsMappingResults->getErrors());
383 $this->clearCacheOnError();
384
385 $errorFlashMessage = $this->getErrorFlashMessage();
386 if ($errorFlashMessage !== FALSE) {
387 $this->flashMessages->add($errorFlashMessage);
388 }
389
390 if ($this->request->hasArgument('__referrer')) {
391 $referrer = $this->request->getArgument('__referrer');
392 $this->forward($referrer['actionName'], $referrer['controllerName'], $referrer['extensionName'], $this->request->getArguments());
393 }
394
395 $message = 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '().' . PHP_EOL;
396 foreach ($this->argumentsMappingResults->getErrors() as $error) {
397 $message .= 'Error: ' . $error->getMessage() . PHP_EOL;
398 }
399 foreach ($this->argumentsMappingResults->getWarnings() as $warning) {
400 $message .= 'Warning: ' . $warning->getMessage() . PHP_EOL;
401 }
402 return $message;
403 }
404
405 /**
406 * A template method for displaying custom error flash messages, or to
407 * display no flash message at all on errors. Override this to customize
408 * the flash message in your action controller.
409 *
410 * @return string|boolean The flash message or FALSE if no flash message should be set
411 * @api
412 */
413 protected function getErrorFlashMessage() {
414 return 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '()';
415 }
416
417 /**
418 * Checks the request hash (HMAC), if arguments have been touched by the property mapper.
419 *
420 * In case the @dontverifyrequesthash-Annotation has been set, this suppresses the exception.
421 *
422 * @return void
423 * @throws Tx_Extbase_MVC_Exception_InvalidOrNoRequestHash In case request hash checking failed
424 * @author Sebastian Kurf├╝rst <sebastian@typo3.org>
425 */
426 protected function checkRequestHash() {
427 if (!($this->request instanceof Tx_Extbase_MVC_Web_Request)) return; // We only want to check it for now for web requests.
428 if ($this->request->isHmacVerified()) return; // all good
429
430 $verificationNeeded = FALSE;
431 foreach ($this->arguments as $argument) {
432 if ($argument->getOrigin() == Tx_Extbase_MVC_Controller_Argument::ORIGIN_NEWLY_CREATED
433 || $argument->getOrigin() == Tx_Extbase_MVC_Controller_Argument::ORIGIN_PERSISTENCE_AND_MODIFIED) {
434 $verificationNeeded = TRUE;
435 }
436 }
437 if ($verificationNeeded) {
438 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
439 if (!isset($methodTagsValues['dontverifyrequesthash'])) {
440 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);
441 }
442 }
443 }
444
445 /**
446 * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
447 * Better would be just do delete the cache for the error action, but that is not possible right now.
448 *
449 * @return void
450 */
451 protected function clearCacheOnError() {
452 $extbaseSettings = $this->configurationManager->getConfiguration(Tx_Extbase_Configuration_ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
453 if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
454 if (isset($GLOBALS['TSFE'])) {
455 $pageUid = $GLOBALS['TSFE']->id;
456 Tx_Extbase_Utility_Cache::clearPageCache(array($pageUid));
457 }
458 }
459 }
460
461 }
462 ?>