Controller.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2017 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  $this->response->addError($ex->getError());
154  }
155  else {
156  $this->getLogger()->error($ex);
157  $this->response->addError(ApplicationError::fromException($ex));
158  }
159  $this->endTransaction(false);
160  }
161  }
162 
163  // return the last error
164  $errors = array_merge($this->request->getErrors(), $this->response->getErrors());
165  if (sizeof($errors) > 0) {
166  $error = array_pop($errors);
167  $this->response->setValue('errorCode', $error->getCode());
168  $this->response->setValue('errorMessage', $error->getMessage());
169  $this->response->setValue('errorData', $error->getData());
170  $this->response->setStatus($error->getStatusCode());
171  }
172 
173  // log errors
174  for ($i=0,$count=sizeof($errors); $i<$count; $i++) {
175  $error = $errors[$i];
176  $message = $error->__toString();
177  switch ($error->getLevel()) {
179  $this->logger->warn($message);
180  break;
181  default:
182  $this->logger->error($message);
183  }
184  }
185 
186  if ($isDebugEnabled) {
187  $this->logger->debug('Response: '.$this->response);
188  }
189  }
190 
191  /**
192  * Execute the given controller method.
193  * @param $method The name of the method to execute (optional)
194  */
195  protected abstract function doExecute($method=null);
196 
197  /**
198  * Delegate the current request to another action. The context is the same as
199  * the current context and the source controller will be set to this.
200  * The request and response format will be NullFormat which means that all
201  * request values should be passed in the application internal format and
202  * all response values will have that format. Execution will return to the
203  * calling controller instance afterwards.
204  * @param $action The name of the action to execute
205  * @return Response instance
206  */
207  protected function executeSubAction($action) {
208  $curRequest = $this->getRequest();
209  $subRequest = ObjectFactory::getNewInstance('request');
210  $subRequest->setSender(get_class($this));
211  $subRequest->setContext($curRequest->getContext());
212  $subRequest->setAction($action);
213  $subRequest->setHeaders($curRequest->getHeaders());
214  $subRequest->setValues($curRequest->getValues());
215  $subRequest->setFormat('null');
216  $subRequest->setResponseFormat('null');
217  $subResponse = ObjectFactory::getNewInstance('response');
218  $this->actionMapper->processAction($subRequest, $subResponse);
219  return $subResponse;
220  }
221 
222  /**
223  * Redirect to the given location with the given request data externally
224  * (HTTP status code 302). The method will not return a result to the calling
225  * controller method. The calling method should return immediatly in order to
226  * avoid any side effects of code executed after the redirect. The given data
227  * are stored in the session under the given key.
228  * @param $location The location to redirect to
229  * @param $key The key used as session variable name (optional)
230  * @param $data The data to be stored in the session (optional)
231  */
232  protected function redirect($location, $key=null, $data=null) {
233  if (strlen($key) > 0 && $data != null) {
234  $session = $this->getSession();
235  $session->set($key, $data);
236  }
237  $response = $this->getResponse();
238  $response->setHeader('Location', $location);
239  $response->setStatus(302);
240  $response->setFormat('null'); // prevent any rendering
241  }
242 
243  /**
244  * Get the Request instance.
245  * @return Request
246  */
247  public function getRequest() {
248  return $this->request;
249  }
250 
251  /**
252  * Get the Response instance.
253  * @return Response
254  */
255  public function getResponse() {
256  return $this->response;
257  }
258 
259  /**
260  * Get the Logger instance.
261  * @return Logger
262  */
263  protected function getLogger() {
264  return $this->logger;
265  }
266 
267  /**
268  * Get the Session instance.
269  * @return Session
270  */
271  protected function getSession() {
272  return $this->session;
273  }
274 
275  /**
276  * Get the PersistenceFacade instance.
277  * @return PersistenceFacade
278  */
279  protected function getPersistenceFacade() {
280  return $this->persistenceFacade;
281  }
282 
283  /**
284  * Get the PermissionManager instance.
285  * @return PermissionManager
286  */
287  protected function getPermissionManager() {
288  return $this->permissionManager;
289  }
290 
291  /**
292  * Get the ActionMapper instance.
293  * @return ActionMapper
294  */
295  protected function getActionMapper() {
296  return $this->actionMapper;
297  }
298 
299  /**
300  * Get the Localization instance.
301  * @return Localization
302  */
303  protected function getLocalization() {
304  return $this->localization;
305  }
306 
307  /**
308  * Get the Message instance.
309  * @return Message
310  */
311  protected function getMessage() {
312  return $this->message;
313  }
314 
315  /**
316  * Get the Configuration instance.
317  * @return Configuration
318  */
319  protected function getConfiguration() {
320  return $this->configuration;
321  }
322 
323  /**
324  * Start or join a transaction that will be committed at the end of execution.
325  * If a transaction already is started it will be joined and committed by the
326  * controller that started it. This allows to compose actions by using the
327  * Controller::executeSubAction() method that all share the same transaction.
328  * @return Boolean whether a new transaction was started or an existing was joined
329  */
330  protected function requireTransaction() {
331  $tx = $this->getPersistenceFacade()->getTransaction();
332  if (!$tx->isActive()) {
333  $tx->begin();
334  $this->startedTransaction = true;
335  }
336  }
337 
338  /**
339  * End the transaction. Only if this controller instance started the transaction,
340  * it will be committed or rolled back. Otherwise the call will be ignored.
341  * @param $commit Boolean whether the transaction should be committed
342  */
343  protected function endTransaction($commit) {
344  $tx = $this->getPersistenceFacade()->getTransaction();
345  if ($this->startedTransaction && $tx->isActive()) {
346  if ($commit) {
347  $tx->commit();
348  }
349  else {
350  $tx->rollback();
351  }
352  }
353  $this->startedTransaction = false;
354  }
355 
356  /**
357  * Check if the current request is localized. This is true,
358  * if it has a language parameter that is not equal to Localization::getDefaultLanguage().
359  * Throws an exception if a language is given which is not supported
360  * @return Boolean whether the request is localized or not
361  */
362  protected function isLocalizedRequest() {
363  if ($this->request->hasValue('language')) {
364  $language = $this->request->getValue('language');
365  if ($language != $this->localization->getDefaultLanguage()) {
366  return true;
367  }
368  }
369  return false;
370  }
371 
372  /**
373  * Checks the language request parameter and adds an response error,
374  * if it is not contained in the Localization::getSupportedLanguages() list.
375  * @return Boolean
376  */
377  protected function checkLanguageParameter() {
378  if ($this->request->hasValue('language')) {
379  $language = $this->request->getValue('language');
380  if (!in_array($language, array_keys($this->localization->getSupportedLanguages()))) {
381  $this->response->addError(ApplicationError::get('PARAMETER_INVALID',
382  ['invalidParameters' => ['language']]));
383  return false;
384  }
385  }
386  return true;
387  }
388 
389  /**
390  * Create a CSRF token, store it in the session and set it in the response.
391  * The name of the response parameter is Controller::CSRF_TOKEN_PARAM.
392  * @param $name The name of the token to be used in Controller::validateCsrfToken()
393  */
394  protected function generateCsrfToken($name) {
395  // generate token and store in session
396  $token = base64_encode(openssl_random_pseudo_bytes(32));
397  $this->getSession()->set(self::CSRF_TOKEN_PARAM.'_'.$name, $token);
398 
399  // set token in response
400  $response = $this->getResponse();
401  $response->setValue(self::CSRF_TOKEN_PARAM, $token);
402  }
403 
404  /**
405  * Validate the CSRF token contained in the request against the token stored
406  * in the session. The name of the request parameter is Controller::CSRF_TOKEN_PARAM.
407  * @param $name The name of the token as set in Controller::generateCsrfToken()
408  * @return boolean
409  */
410  protected function validateCsrfToken($name) {
411  // get token from session
412  $session = $this->getSession();
413  $tokenKey = self::CSRF_TOKEN_PARAM.'_'.$name;
414  if (!$session->exist($tokenKey)) {
415  return false;
416  }
417  $storedToken = $session->get($tokenKey);
418  $session->remove($tokenKey);
419 
420  // compare session token with request token
421  $token = $this->getRequest()->getValue(self::CSRF_TOKEN_PARAM);
422  return $token === $storedToken;
423  }
424 
425  /**
426  * Set the value of a local session variable.
427  * @param $key The key (name) of the session vaiable.
428  * @param $default The default value if the key is not defined (optional, default: _null_)
429  * @return The session var or null if it doesn't exist.
430  */
431  protected function getLocalSessionValue($key, $default=null) {
432  $sessionVarname = get_class($this);
433  $localValues = $this->session->get($sessionVarname, null);
434  return array_key_exists($key, $localValues) ? $localValues[$key] : $default;
435  }
436 
437  /**
438  * Get the value of a local session variable.
439  * @param $key The key (name) of the session vaiable.
440  * @param $value The value of the session variable.
441  */
442  protected function setLocalSessionValue($key, $value) {
443  $sessionVarname = get_class($this);
444  $localValues = $this->session->get($sessionVarname, null);
445  if ($localValues == null) {
446  $localValues = [];
447  }
448  $localValues[$key] = $value;
449  $this->session->set($sessionVarname, $localValues);
450  }
451 
452  /**
453  * Remove all local session values.
454  * @param $key The key (name) of the session vaiable.
455  * @param $value The value of the session variable.
456  */
457  protected function clearLocalSessionValues() {
458  $sessionVarname = get_class($this);
459  $this->session->remove($sessionVarname);
460  }
461 }
462 ?>
redirect($location, $key=null, $data=null)
Redirect to the given location with the given request data externally (HTTP status code 302)...
Definition: Controller.php:232
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
doExecute($method=null)
Execute the given controller method.
getRequest()
Get the Request instance.
Definition: Controller.php:247
Localization defines the interface for storing localized entity instances and retrieving them back...
getMessage()
Get the Message instance.
Definition: Controller.php:311
Controller is the base class of all controllers.
Definition: Controller.php:49
endTransaction($commit)
End the transaction.
Definition: Controller.php:343
setSender($sender)
Set the name of the sending Controller.
Presentation related interfaces and classes.
Definition: namespaces.php:59
getPermissionManager()
Get the PermissionManager instance.
Definition: Controller.php:287
clearLocalSessionValues()
Remove all local session values.
Definition: Controller.php:457
isLocalizedRequest()
Check if the current request is localized.
Definition: Controller.php:362
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
getLocalization()
Get the Localization instance.
Definition: Controller.php:303
getLocalSessionValue($key, $default=null)
Set the value of a local session variable.
Definition: Controller.php:431
validateCsrfToken($name)
Validate the CSRF token contained in the request against the token stored in the session.
Definition: Controller.php:410
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23
checkLanguageParameter()
Checks the language request parameter and adds an response error, if it is not contained in the Local...
Definition: Controller.php:377
Session is the interface for session implementations and defines access to session variables...
Definition: Session.php:19
getConfiguration()
Get the Configuration instance.
Definition: Controller.php:319
generateCsrfToken($name)
Create a CSRF token, store it in the session and set it in the response.
Definition: Controller.php:394
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:18
setLocalSessionValue($key, $value)
Get the value of a local session variable.
Definition: Controller.php:442
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
executeSubAction($action)
Delegate the current request to another action.
Definition: Controller.php:207
PermissionManager implementations are used to handle all authorization requests.
Implementations of Configuration give access to the application configuration.
requireTransaction()
Start or join a transaction that will be committed at the end of execution.
Definition: Controller.php:330
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
static get($code, $data=null)
Factory method for retrieving a predefined error instance.
execute($method=null)
Execute the Controller resulting in its action processed.
Definition: Controller.php:132
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration)
Constructor.
Definition: Controller.php:77
initialize(Request $request, Response $response)
Initialize the Controller with request/response data.
Definition: Controller.php:108
PersistenceFacade defines the interface for PersistenceFacade implementations.
ApplicationException signals a general application exception.
getLogger()
Get the Logger instance.
Definition: Controller.php:263
getActionMapper()
Get the ActionMapper instance.
Definition: Controller.php:295
getResponse()
Get the Response instance.
Definition: Controller.php:255
getSession()
Get the Session instance.
Definition: Controller.php:271
static getNewInstance($name, $dynamicConfiguration=[])
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:279