Controller.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\presentation;
12 
24 
25 /**
26  * Controller is the base class of all controllers.
27  *
28  * Error Handling:
29  * - throw an Exception or use response action _failure_ to signal fatal errors
30  * (calls wcmf::application::controller::FailureController)
31  * - add an ApplicationError to the response to signal non fatal errors (e.g.
32  * validation errors)
33  *
34  * The following default request/response parameters are defined:
35  *
36  * | Parameter | Description
37  * |-------------------------|-------------------------
38  * | _in_ / _out_ `action` | The action to be executed
39  * | _in_ / _out_ `context` | The context of the action
40  * | _in_ `language` | The language of the requested data (optional)
41  * | _out_ `controller` | The name of the executed controller
42  * | _out_ `success` | Boolean whether the action completed successfully or not (depends on existence of error messages)
43  * | _out_ `errorMessage` | An error message which is displayed to the user
44  * | _out_ `errorCode` | An error code, describing the type of error
45  * | _out_ `errorData` | Some error codes require to transmit further information to the client
46  *
47  * @author ingo herwig <ingo@wemove.com>
48  */
49 abstract class Controller {
50 
51  const CSRF_TOKEN_PARAM = 'csrf_token';
52 
53  private $request = null;
54  private $response = null;
55 
56  private $logger = null;
57  private $session = null;
58  private $persistenceFacade = null;
59  private $permissionManager = null;
60  private $actionMapper = null;
61  private $localization = null;
62  private $message = null;
63  private $configuration = null;
64 
65  private $startedTransaction = false;
66 
67  /**
68  * Constructor
69  * @param $session
70  * @param $persistenceFacade
71  * @param $permissionManager
72  * @param $actionMapper
73  * @param $localization
74  * @param $message
75  * @param $configuration
76  */
77  public function __construct(Session $session,
78  PersistenceFacade $persistenceFacade,
79  PermissionManager $permissionManager,
80  ActionMapper $actionMapper,
81  Localization $localization,
82  Message $message,
83  Configuration $configuration) {
84  $this->logger = LogManager::getLogger(get_class($this));
85  $this->session = $session;
86  $this->persistenceFacade = $persistenceFacade;
87  $this->permissionManager = $permissionManager;
88  $this->actionMapper = $actionMapper;
89  $this->localization = $localization;
90  $this->message = $message;
91  $this->configuration = $configuration;
92  }
93 
94  /**
95  * Initialize the Controller with request/response data. Which data is required is defined by the Controller.
96  * The base class method just stores the parameters in a member variable. Specialized Controllers may override
97  * this behavior for further initialization.
98  * @attention It lies in its responsibility to fail or do some default action if some data is missing.
99  * @param $request Request instance sent to the Controller. The sender attribute of the Request is the
100  * last controller's name, the context is the current context and the action is the requested one.
101  * All data sent from the last controller are accessible using the Request::getValue method. The request is
102  * supposed to be read-only. It will not be used any more after being passed to the controller.
103  * @param $response Response instance that will be modified by the Controller. The initial values for
104  * context and action are the same as in the request parameter and are meant to be modified according to the
105  * performed action. The sender attribute of the response is set to the current controller. Initially there
106  * are no data stored in the response.
107  */
108  public function initialize(Request $request, Response $response) {
109  // set sender on response
110  $response->setSender(get_class($this));
111 
112  $this->request = $request;
113  $this->response = $response;
114  }
115 
116  /**
117  * Check if the request is valid.
118  * Subclasses will override this method to validate against their special requirements.
119  * Besides returning false, validation errors should be indicated by using the
120  * Response::addError method.
121  * @return Boolean whether the data are ok or not.
122  */
123  protected function validate() {
124  return true;
125  }
126 
127  /**
128  * Execute the Controller resulting in its action processed. The actual
129  * processing is delegated to the doExecute() method.
130  * @param $method The name of the method to execute, will be passed to doExecute() (optional)
131  */
132  public function execute($method=null) {
133  $isDebugEnabled = $this->logger->isDebugEnabled();
134  if ($isDebugEnabled) {
135  $this->logger->debug('Executing: '.get_class($this).($method ? '::'.$method: ''));
136  $this->logger->debug('Request: '.$this->request);
137  }
138 
139  // validate controller data
140  $validationFailed = false;
141  if (!$this->validate()) {
142  $validationFailed = true;
143  }
144 
145  // execute controller logic
146  if (!$validationFailed) {
147  try {
148  $this->doExecute($method);
149  $this->endTransaction(true);
150  }
151  catch (\Exception $ex) {
152  if ($ex instanceof ApplicationException) {
153  $error = $ex->getError();
154  $this->response->addError($error);
155  $this->response->setStatus($error->getStatusCode());
156  }
157  else {
158  $this->getLogger()->error($ex);
159  $error = ApplicationError::fromException($ex);
160  $this->response->addError($error);
161  $this->response->setStatus($error->getStatusCode());
162  }
163  $this->endTransaction(false);
164  }
165  }
166 
167  // return the last error
168  $errors = array_merge($this->request->getErrors(), $this->response->getErrors());
169  if (sizeof($errors) > 0) {
170  $error = array_pop($errors);
171  $this->response->setValue('errorCode', $error->getCode());
172  $this->response->setValue('errorMessage', $error->getMessage());
173  $this->response->setValue('errorData', $error->getData());
174  $this->response->setStatus($error->getStatusCode());
175  }
176 
177  // log errors
178  for ($i=0,$count=sizeof($errors); $i<$count; $i++) {
179  $error = $errors[$i];
180  $message = $error->__toString();
181  switch ($error->getLevel()) {
183  $this->logger->warn($message);
184  break;
185  default:
186  $this->logger->error($message);
187  }
188  }
189 
190  if ($isDebugEnabled) {
191  $this->logger->debug('Response: '.$this->response);
192  }
193  }
194 
195  /**
196  * Execute the given controller method.
197  * @param $method The name of the method to execute (optional)
198  */
199  protected abstract function doExecute($method=null);
200 
201  /**
202  * Delegate the current request to another action. The context is the same as
203  * the current context and the source controller will be set to this.
204  * The request and response format will be NullFormat which means that all
205  * request values should be passed in the application internal format and
206  * all response values will have that format. Execution will return to the
207  * calling controller instance afterwards.
208  * @param $action The name of the action to execute
209  * @return Response instance
210  */
211  protected function executeSubAction($action) {
212  $curRequest = $this->getRequest();
213  $subRequest = ObjectFactory::getNewInstance('request');
214  $subRequest->setSender(get_class($this));
215  $subRequest->setContext($curRequest->getContext());
216  $subRequest->setAction($action);
217  $subRequest->setHeaders($curRequest->getHeaders());
218  $subRequest->setValues($curRequest->getValues());
219  $subRequest->setFormat('null');
220  $subRequest->setResponseFormat('null');
221  $subResponse = ObjectFactory::getNewInstance('response');
222  $this->actionMapper->processAction($subRequest, $subResponse);
223  return $subResponse;
224  }
225 
226  /**
227  * Redirect to the given location with the given request data externally
228  * (HTTP status code 302). The method will not return a result to the calling
229  * controller method. The calling method should return immediatly in order to
230  * avoid any side effects of code executed after the redirect. The given data
231  * are stored in the session under the given key.
232  * @param $location The location to redirect to
233  * @param $key The key used as session variable name (optional)
234  * @param $data The data to be stored in the session (optional)
235  */
236  protected function redirect($location, $key=null, $data=null) {
237  if (strlen($key) > 0 && $data != null) {
238  $session = $this->getSession();
239  $session->set($key, $data);
240  }
241  $response = $this->getResponse();
242  $response->setHeader('Location', $location);
243  $response->setStatus(302);
244  $response->setFormat('null'); // prevent any rendering
245  }
246 
247  /**
248  * Get the Request instance.
249  * @return Request
250  */
251  public function getRequest() {
252  return $this->request;
253  }
254 
255  /**
256  * Get the Response instance.
257  * @return Response
258  */
259  public function getResponse() {
260  return $this->response;
261  }
262 
263  /**
264  * Get the Logger instance.
265  * @return Logger
266  */
267  protected function getLogger() {
268  return $this->logger;
269  }
270 
271  /**
272  * Get the Session instance.
273  * @return Session
274  */
275  protected function getSession() {
276  return $this->session;
277  }
278 
279  /**
280  * Get the PersistenceFacade instance.
281  * @return PersistenceFacade
282  */
283  protected function getPersistenceFacade() {
284  return $this->persistenceFacade;
285  }
286 
287  /**
288  * Get the PermissionManager instance.
289  * @return PermissionManager
290  */
291  protected function getPermissionManager() {
292  return $this->permissionManager;
293  }
294 
295  /**
296  * Get the ActionMapper instance.
297  * @return ActionMapper
298  */
299  protected function getActionMapper() {
300  return $this->actionMapper;
301  }
302 
303  /**
304  * Get the Localization instance.
305  * @return Localization
306  */
307  protected function getLocalization() {
308  return $this->localization;
309  }
310 
311  /**
312  * Get the Message instance.
313  * @return Message
314  */
315  protected function getMessage() {
316  return $this->message;
317  }
318 
319  /**
320  * Get the Configuration instance.
321  * @return Configuration
322  */
323  protected function getConfiguration() {
324  return $this->configuration;
325  }
326 
327  /**
328  * Start or join a transaction that will be committed at the end of execution.
329  * If a transaction already is started it will be joined and committed by the
330  * controller that started it. This allows to compose actions by using the
331  * Controller::executeSubAction() method that all share the same transaction.
332  * @return Boolean whether a new transaction was started or an existing was joined
333  */
334  protected function requireTransaction() {
335  $tx = $this->getPersistenceFacade()->getTransaction();
336  if (!$tx->isActive()) {
337  $tx->begin();
338  $this->startedTransaction = true;
339  }
340  }
341 
342  /**
343  * End the transaction. Only if this controller instance started the transaction,
344  * it will be committed or rolled back. Otherwise the call will be ignored.
345  * @param $commit Boolean whether the transaction should be committed
346  */
347  protected function endTransaction($commit) {
348  $tx = $this->getPersistenceFacade()->getTransaction();
349  if ($this->startedTransaction && $tx->isActive()) {
350  if ($commit) {
351  $tx->commit();
352  }
353  else {
354  $tx->rollback();
355  }
356  }
357  $this->startedTransaction = false;
358  }
359 
360  /**
361  * Check if the current request is localized. This is true,
362  * if it has a language parameter that is not equal to Localization::getDefaultLanguage().
363  * Throws an exception if a language is given which is not supported
364  * @return Boolean whether the request is localized or not
365  */
366  protected function isLocalizedRequest() {
367  if ($this->request->hasValue('language')) {
368  $language = $this->request->getValue('language');
369  if ($language != $this->localization->getDefaultLanguage()) {
370  return true;
371  }
372  }
373  return false;
374  }
375 
376  /**
377  * Checks the language request parameter and adds an response error,
378  * if it is not contained in the Localization::getSupportedLanguages() list.
379  * @return Boolean
380  */
381  protected function checkLanguageParameter() {
382  if ($this->request->hasValue('language')) {
383  $language = $this->request->getValue('language');
384  if (!in_array($language, array_keys($this->localization->getSupportedLanguages()))) {
385  $this->response->addError(ApplicationError::get('PARAMETER_INVALID',
386  ['invalidParameters' => ['language']]));
387  return false;
388  }
389  }
390  return true;
391  }
392 
393  /**
394  * Create a CSRF token, store it in the session and set it in the response.
395  * The name of the response parameter is Controller::CSRF_TOKEN_PARAM.
396  * @param $name The name of the token to be used in Controller::validateCsrfToken()
397  * @param $refresh Boolean indicating whether an existing token should be invalidated or reused (optional, default: _true_)
398  */
399  protected function generateCsrfToken($name, $refresh=true) {
400  $session = $this->getSession();
401  $token = $session->get(self::CSRF_TOKEN_PARAM.'_'.$name);
402  if (!$token || $refresh) {
403  // generate token and store in session
404  $token = base64_encode(openssl_random_pseudo_bytes(32));
405  $session->set(self::CSRF_TOKEN_PARAM.'_'.$name, $token);
406  }
407 
408  // set token in response
409  $response = $this->getResponse();
410  $response->setValue(self::CSRF_TOKEN_PARAM, $token);
411  }
412 
413  /**
414  * Validate the CSRF token contained in the request against the token stored
415  * in the session. The name of the request parameter is Controller::CSRF_TOKEN_PARAM.
416  * @param $name The name of the token as set in Controller::generateCsrfToken()
417  * @param $invalidate Boolean whether to delete the stored token or not (optional: default: _true_)
418  * @return boolean
419  */
420  protected function validateCsrfToken($name, $invalidate=true) {
421  // get token from session
422  $session = $this->getSession();
423  $tokenKey = self::CSRF_TOKEN_PARAM.'_'.$name;
424  if (!$session->exist($tokenKey)) {
425  return false;
426  }
427  $storedToken = $session->get($tokenKey);
428  if ($invalidate) {
429  $session->remove($tokenKey);
430  }
431 
432  // compare session token with request token
433  $token = $this->getRequest()->getValue(self::CSRF_TOKEN_PARAM);
434  return $token === $storedToken;
435  }
436 
437  /**
438  * Set the value of a local session variable.
439  * @param $key The key (name) of the session vaiable.
440  * @param $default The default value if the key is not defined (optional, default: _null_)
441  * @return The session var or null if it doesn't exist.
442  */
443  protected function getLocalSessionValue($key, $default=null) {
444  $sessionVarname = get_class($this);
445  $localValues = $this->session->get($sessionVarname, null);
446  return array_key_exists($key, $localValues) ? $localValues[$key] : $default;
447  }
448 
449  /**
450  * Get the value of a local session variable.
451  * @param $key The key (name) of the session vaiable.
452  * @param $value The value of the session variable.
453  */
454  protected function setLocalSessionValue($key, $value) {
455  $sessionVarname = get_class($this);
456  $localValues = $this->session->get($sessionVarname, null);
457  if ($localValues == null) {
458  $localValues = [];
459  }
460  $localValues[$key] = $value;
461  $this->session->set($sessionVarname, $localValues);
462  }
463 
464  /**
465  * Remove all local session values.
466  * @param $key The key (name) of the session vaiable.
467  * @param $value The value of the session variable.
468  */
469  protected function clearLocalSessionValues() {
470  $sessionVarname = get_class($this);
471  $this->session->remove($sessionVarname);
472  }
473 }
474 ?>
execute($method=null)
Execute the Controller resulting in its action processed.
Definition: Controller.php:132
Session is the interface for session implementations and defines access to session variables.
Definition: Session.php:19
setSender($sender)
Set the name of the sending Controller.
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:18
getConfiguration()
Get the Configuration instance.
Definition: Controller.php:323
executeSubAction($action)
Delegate the current request to another action.
Definition: Controller.php:211
static fromException(\Exception $ex)
Factory method for transforming an exception into an ApplicationError instance.
validate()
Check if the request is valid.
Definition: Controller.php:123
initialize(Request $request, Response $response)
Initialize the Controller with request/response data.
Definition: Controller.php:108
endTransaction($commit)
End the transaction.
Definition: Controller.php:347
clearLocalSessionValues()
Remove all local session values.
Definition: Controller.php:469
getLocalSessionValue($key, $default=null)
Set the value of a local session variable.
Definition: Controller.php:443
generateCsrfToken($name, $refresh=true)
Create a CSRF token, store it in the session and set it in the response.
Definition: Controller.php:399
getPermissionManager()
Get the PermissionManager instance.
Definition: Controller.php:291
requireTransaction()
Start or join a transaction that will be committed at the end of execution.
Definition: Controller.php:334
Implementations of Configuration give access to the application configuration.
getLogger()
Get the Logger instance.
Definition: Controller.php:267
getMessage()
Get the Message instance.
Definition: Controller.php:315
redirect($location, $key=null, $data=null)
Redirect to the given location with the given request data externally (HTTP status code 302).
Definition: Controller.php:236
setLocalSessionValue($key, $value)
Get the value of a local session variable.
Definition: Controller.php:454
ApplicationError is used to signal errors that occur while processing a request.
static get($code, $data=null)
Factory method for retrieving a predefined error instance.
PersistenceFacade defines the interface for PersistenceFacade implementations.
ApplicationException signals a general application exception.
getActionMapper()
Get the ActionMapper instance.
Definition: Controller.php:299
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:283
getRequest()
Get the Request instance.
Definition: Controller.php:251
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
doExecute($method=null)
Execute the given controller method.
static getNewInstance($name, $dynamicConfiguration=[])
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration)
Constructor.
Definition: Controller.php:77
getLocalization()
Get the Localization instance.
Definition: Controller.php:307
Controller is the base class of all controllers.
Definition: Controller.php:49
isLocalizedRequest()
Check if the current request is localized.
Definition: Controller.php:366
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
getResponse()
Get the Response instance.
Definition: Controller.php:259
PermissionManager implementations are used to handle all authorization requests.
LogManager is used to retrieve Logger instances.
Definition: LogManager.php:20
getSession()
Get the Session instance.
Definition: Controller.php:275
ObjectFactory implements the service locator pattern by wrapping a Factory instance and providing sta...
validateCsrfToken($name, $invalidate=true)
Validate the CSRF token contained in the request against the token stored in the session.
Definition: Controller.php:420
Localization defines the interface for storing localized entity instances and retrieving them back.
Presentation related interfaces and classes.
Definition: namespaces.php:59
checkLanguageParameter()
Checks the language request parameter and adds an response error, if it is not contained in the Local...
Definition: Controller.php:381
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23