RESTController.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  */
12 
25 use wcmf\lib\presentation\ControllerMethods;
29 
30 /**
31  * RESTController handles requests sent from a dstore/Rest client.
32  * @see http://dstorejs.io
33  *
34  * The controller supports the following actions:
35  *
36  * <div class="controller-action">
37  * <div> __Action__ _default_ </div>
38  * <div>
39  * Handle action according to HTTP method and parameters.
40  *
41  * For details about the parameters, see documentation of the methods.
42  *
43  * | __Response Actions__ | |
44  * |------------------------|-------------------------
45  * | `ok` | In all cases
46  * </div>
47  * </div>
48  *
49  * @author ingo herwig <ingo@wemove.com>
50  */
51 class RESTController extends Controller {
52  use ControllerMethods;
53 
54  private $eventManager = null;
55 
56  /**
57  * Constructor
58  * @param $session
59  * @param $persistenceFacade
60  * @param $permissionManager
61  * @param $actionMapper
62  * @param $localization
63  * @param $message
64  * @param $configuration
65  * @param $eventManager
66  */
67  public function __construct(Session $session,
68  PersistenceFacade $persistenceFacade,
69  PermissionManager $permissionManager,
70  ActionMapper $actionMapper,
71  Localization $localization,
72  Message $message,
73  Configuration $configuration,
74  EventManager $eventManager) {
75  parent::__construct($session, $persistenceFacade, $permissionManager,
76  $actionMapper, $localization, $message, $configuration);
77  $this->eventManager = $eventManager;
78  // add transaction listener
79  $this->eventManager->addListener(TransactionEvent::NAME, [$this, 'afterCommit']);
80  }
81 
82  /**
83  * Destructor
84  */
85  public function __destruct() {
86  $this->eventManager->removeListener(TransactionEvent::NAME, [$this, 'afterCommit']);
87  }
88 
89  /**
90  * @see Controller::initialize()
91  */
92  public function initialize(Request $request, Response $response) {
93  // construct oid from className and id
94  if ($request->hasValue('className') && $request->hasValue('id')) {
95  $oid = new ObjectId($request->getValue('className'), $request->getValue('id'));
96  $request->setValue('oid', $oid->__toString());
97  }
98  // construct sourceOid from className and sourceId
99  if ($request->hasValue('className') && $request->hasValue('sourceId')) {
100  $sourceOid = new ObjectId($request->getValue('className'), $request->getValue('sourceId'));
101  $request->setValue('sourceOid', $sourceOid->__toString());
102  }
103  // construct oid, targetOid from sourceOid, relation and targetId
104  if ($request->hasValue('sourceOid') && $request->hasValue('relation') && $request->hasValue('targetId')) {
105  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
106  $relatedType = $this->getRelatedType($sourceOid, $request->getValue('relation'));
107  $targetOid = new ObjectId($relatedType, $request->getValue('targetId'));
108  $request->setValue('targetOid', $targetOid->__toString());
109  // non-collection requests
110  $request->setValue('oid', $targetOid->__toString());
111  }
112  parent::initialize($request, $response);
113  }
114 
115  /**
116  * @see Controller::validate()
117  */
118  protected function validate() {
119  $request = $this->getRequest();
120  $response = $this->getResponse();
121  if ($request->hasValue('className') &&
122  !$this->getPersistenceFacade()->isKnownType($request->getValue('className')))
123  {
124  $response->addError(ApplicationError::get('PARAMETER_INVALID',
125  ['invalidParameters' => ['className']]));
126  return false;
127  }
128  if ($request->hasHeader('Position')) {
129  $position = $request->getHeader('Position');
130  if (!preg_match('/^before /', $position) && !preg_match('/^last$/', $position)) {
131  $response->addError(ApplicationError::get('PARAMETER_INVALID',
132  ['invalidParameters' => ['Position']]));
133  return false;
134  }
135  }
136  // do default validation
137  return parent::validate();
138  }
139 
140  /**
141  * Read an object
142  *
143  * | Parameter | Description
144  * |------------------|-------------------------
145  * | _in_ `language` | The language of the returned object
146  * | _in_ `className` | The type of returned object
147  * | _in_ `id` | The id of returned object
148  * | _out_ | Single object
149  */
150  public function read() {
151  // delegate to DisplayController
152  $subResponse = $this->executeSubAction('read');
153  $this->handleSubResponse($subResponse);
154  }
155 
156  /**
157  * Read objects of a given type
158  *
159  * | Parameter | Description
160  * |------------------|-------------------------
161  * | _in_ `language` | The language of the returned objects
162  * | _in_ `className` | The type of returned objects
163  * | _in_ `sortBy` | <em>?sortBy=+foo</em> for sorting the list by foo ascending or
164  * | _in_ `sort` | <em>?sort(+foo)</em> for sorting the list by foo ascending
165  * | _in_ `limit` | <em>?limit(10,25)</em> for loading 10 objects starting from position 25
166  * | _in_ `query` | A query condition encoded in RQL to be used with StringQuery::setRQLConditionString()
167  * | _out_ | List of objects
168  */
169  public function readList() {
170  // delegate to ListController
171  $subResponse = $this->executeSubAction('list');
172  $this->handleSubResponse($subResponse);
173  }
174 
175  /**
176  * Read objects that are related to another object
177  *
178  * | Parameter | Description
179  * |------------------|-------------------------
180  * | _in_ `language` | The language of the returned objects
181  * | _in_ `sourceId` | Id of the object to which the returned objects are related (determines the object id together with _className_)
182  * | _in_ `className` | The type of the object defined by _sourceId_
183  * | _in_ `relation` | Name of the relation to the object defined by _sourceId_ (determines the type of the returned objects)
184  * | _in_ `sortBy` | <em>?sortBy=+foo</em> for sorting the list by foo ascending or
185  * | _in_ `sort` | <em>?sort(+foo)</em> for sorting the list by foo ascending
186  * | _in_ `limit` | <em>?limit(10,25)</em> for loading 10 objects starting from position 25
187  * | _in_ `query` | A query condition encoded in RQL to be used with StringQuery::setRQLConditionString()
188  * | _out_ | List of objects
189  */
190  public function readInRelation() {
191  $request = $this->getRequest();
192 
193  // rewrite query if querying for a relation
194  $relationName = $request->getValue('relation');
195 
196  // set the query
197  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
198  $persistenceFacade = $this->getPersistenceFacade();
199  $sourceNode = $persistenceFacade->load($sourceOid);
200  if ($sourceNode) {
201  $relationQuery = NodeUtil::getRelationQueryCondition($sourceNode, $relationName);
202  $query = ($request->hasValue('query') ? $request->getValue('query').'&' : '').$relationQuery;
203  $request->setValue('query', $query);
204 
205  // set the class name
206  $mapper = $sourceNode->getMapper();
207  $relation = $mapper->getRelation($relationName);
208  $otherType = $relation->getOtherType();
209  $request->setValue('className', $otherType);
210 
211  // set default order
212  if (!$request->hasValue('sortFieldName')) {
213  $otherMapper = $persistenceFacade->getMapper($otherType);
214  $sortkeyDef = $otherMapper->getSortkey($relation->getThisRole());
215  if ($sortkeyDef != null) {
216  $request->setValue('sortFieldName', $sortkeyDef['sortFieldName']);
217  $request->setValue('sortDirection', $sortkeyDef['sortDirection']);
218  }
219  }
220  }
221 
222  // delegate to ListController
223  $subResponse = $this->executeSubAction('list');
224  $this->handleSubResponse($subResponse);
225  }
226 
227  /**
228  * Create an object of a given type
229  *
230  * | Parameter | Description
231  * |------------------|-------------------------
232  * | _in_ `language` | The language of the object
233  * | _in_ `className` | Type of object to create
234  * | _out_ | Created object data
235  *
236  * The object data is contained in POST content.
237  */
238  public function create() {
239  $this->requireTransaction();
240  // delegate to SaveController
241  $subResponse = $this->executeSubAction('create');
242 
243  // return object only
244  $oidStr = $subResponse->hasValue('oid') ? $subResponse->getValue('oid')->__toString() : null;
245  $this->handleSubResponse($subResponse, $oidStr);
246 
247  // prevent commit
248  if ($subResponse->hasErrors()) {
249  $this->endTransaction(false);
250  }
251  }
252 
253  /**
254  * Create an object of a given type in the given relation
255  *
256  * | Parameter | Description
257  * |------------------|-------------------------
258  * | _in_ `language` | The language of the object
259  * | _in_ `className` | Type of object to create
260  * | _in_ `sourceId` | Id of the object to which the created objects are added (determines the object id together with _className_)
261  * | _in_ `relation` | Name of the relation to the object defined by _sourceId_ (determines the type of the created/added object)
262  * | _out_ | Created object data
263  *
264  * The object data is contained in POST content. If an existing object
265  * should be added, an `oid` parameter in the object data is sufficient.
266  */
267  public function createInRelation() {
268  $this->requireTransaction();
269  $request = $this->getRequest();
270 
271  // create new object
272  $oidStr = null;
273  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
274  $relatedType = $this->getRelatedType($sourceOid, $request->getValue('relation'));
275  $request->setValue('className', $relatedType);
276  $subResponse = $this->executeSubAction('create');
277  if (!$subResponse->hasErrors()) {
278  $createStatus = $subResponse->getStatus();
279  $targetOid = $subResponse->getValue('oid');
280  $targetOidStr = $targetOid->__toString();
281 
282  // add new object to relation
283  $request->setValue('targetOid', $targetOidStr);
284  $request->setValue('role', $request->getValue('relation'));
285  $subResponse = $this->executeSubAction('associate');
286  if (!$subResponse->hasErrors()) {
287  // add related object to subresponse similar to default update action
288  $persistenceFacade = $this->getPersistenceFacade();
289  $targetObj = $persistenceFacade->load($targetOid);
290  $subResponse->setValue('oid', $targetOid);
291  $subResponse->setValue($targetOidStr, $targetObj);
292  $subResponse->setStatus($createStatus);
293 
294  // in case of success, return object only
295  $oidStr = $subResponse->hasValue('oid') ? $subResponse->getValue('oid')->__toString() : '';
296  }
297  }
298 
299  // prevent commit
300  if ($subResponse->hasErrors()) {
301  $this->endTransaction(false);
302  }
303  $this->handleSubResponse($subResponse, $oidStr);
304  }
305 
306  /**
307  * Update an object or change the order
308  *
309  * | Parameter | Description
310  * |------------------|-------------------------
311  * | _in_ `language` | The language of the object
312  * | _in_ `className` | Type of object to update
313  * | _in_ `id` | Id of object to update
314  * | _out_ | Updated object data
315  *
316  * The object data is contained in POST content.
317  */
318  public function update() {
319  $this->requireTransaction();
320  $request = $this->getRequest();
321 
322  $oidStr = $this->getFirstRequestOid();
323  $isOrderRequest = $request->hasValue('referenceOid');
324  if ($isOrderRequest) {
325  // change order in all objects of the same type
326  $request->setValue('insertOid', $this->getFirstRequestOid());
327 
328  // delegate to SortController
329  $subResponse = $this->executeSubAction('moveBefore');
330 
331  // add sorted object to subresponse similar to default update action
332  if (!$subResponse->hasErrors()) {
333  $oid = ObjectId::parse($oidStr);
334  $persistenceFacade = $this->getPersistenceFacade();
335  $object = $persistenceFacade->load($oid);
336  $subResponse->setValue('oid', $oid);
337  $subResponse->setValue($oidStr, $object);
338  }
339  }
340  else {
341  // delegate to SaveController
342  $subResponse = $this->executeSubAction('update');
343  }
344 
345  // prevent commit
346  if ($subResponse->hasErrors()) {
347  $this->endTransaction(false);
348  }
349  $this->handleSubResponse($subResponse, $oidStr);
350  }
351 
352  /**
353  * Update an object in a relation or change the order
354  *
355  * | Parameter | Description
356  * |------------------|-------------------------
357  * | _in_ `language` | The language of the object
358  * | _in_ `className` | Type of object to update
359  * | _in_ `id` | Id of object to update
360  * | _in_ `relation` | Relation name if an existing object should be added to a relation (determines the type of the added object)
361  * | _in_ `sourceId` | Id of the object to which the added object is related (determines the object id together with _className_)
362  * | _in_ `targetId` | Id of the object to be added to the relation (determines the object id together with _relation_)
363  * | _out_ | Updated object data
364  *
365  * The object data is contained in POST content.
366  */
367  public function updateInRelation() {
368  $this->requireTransaction();
369  $request = $this->getRequest();
370 
371  $isOrderRequest = $request->hasValue('referenceOid');
372  $request->setValue('role', $request->getValue('relation'));
373  if ($isOrderRequest) {
374  // change order in a relation
375  $request->setValue('containerOid', $request->getValue('sourceOid'));
376  $request->setValue('insertOid', $request->getValue('targetOid'));
377 
378  // delegate to SortController
379  $subResponse = $this->executeSubAction('insertBefore');
380  }
381  else {
382  // update existing object
383  // delegate to SaveController
384  // NOTE: we need to update first, otherwise the update action might override
385  // the foreign keys changes from the associate action
386  $subResponse = $this->executeSubAction('update');
387  if (!$subResponse->hasErrors()) {
388  // and add object to relation
389  // delegate to AssociateController
390  $subResponse = $this->executeSubAction('associate');
391  }
392  }
393 
394  // add related object to subresponse similar to default update action
395  if (!$subResponse->hasErrors()) {
396  $targetOidStr = $request->getValue('targetOid');
397  $targetOid = ObjectId::parse($targetOidStr);
398  $persistenceFacade = $this->getPersistenceFacade();
399  $targetObj = $persistenceFacade->load($targetOid);
400  $subResponse->setValue('oid', $targetOid);
401  $subResponse->setValue($targetOidStr, $targetObj);
402  }
403 
404  // prevent commit
405  if ($subResponse->hasErrors()) {
406  $this->endTransaction(false);
407  }
408  $this->handleSubResponse($subResponse, $targetOidStr);
409  }
410 
411  /**
412  * Delete an object
413  *
414  * | Parameter | Description
415  * |------------------|-------------------------
416  * | _in_ `language` | The language of the object
417  * | _in_ `className` | Type of object to delete
418  * | _in_ `id` | Id of object to delete
419  */
420  public function delete() {
421  $this->requireTransaction();
422  // delegate to DeleteController
423  $subResponse = $this->executeSubAction('delete');
424 
425  // prevent commit
426  if ($subResponse->hasErrors()) {
427  $this->endTransaction(false);
428  }
429  $this->handleSubResponse($subResponse);
430  }
431 
432  /**
433  * Remove an object from a relation
434  *
435  * | Parameter | Description
436  * |------------------|-------------------------
437  * | _in_ `language` | The language of the object
438  * | _in_ `className` | Type of object to delete
439  * | _in_ `id` | Id of object to delete
440  * | _in_ `relation` | Name of the relation to the object defined by _sourceId_ (determines the type of the deleted object)
441  * | _in_ `sourceId` | Id of the object to which the deleted object is related (determines the object id together with _className_)
442  * | _in_ `targetId` | Id of the object to be deleted from the relation (determines the object id together with _relation_)
443  */
444  public function deleteInRelation() {
445  $this->requireTransaction();
446  $request = $this->getRequest();
447 
448  // remove existing object from relation
449  $request->setValue('role', $request->getValue('relation'));
450 
451  // delegate to AssociateController
452  $subResponse = $this->executeSubAction('disassociate');
453 
454  // prevent commit
455  if ($subResponse->hasErrors()) {
456  $this->endTransaction(false);
457  }
458  $this->handleSubResponse($subResponse);
459  }
460 
461  /**
462  * Create the actual response from the response resulting from delegating to
463  * another controller
464  * @param $subResponse The response returned from the other controller
465  * @param $oidStr Serialized object id of the object to return (optional)
466  * @return Boolean whether an error occured or not
467  */
468  protected function handleSubResponse(Response $subResponse, $oidStr=null) {
469  $response = $this->getResponse();
470  if (!$subResponse->hasErrors()) {
471  $response->clearValues();
472  $response->setHeaders($subResponse->getHeaders());
473  $response->setStatus($subResponse->getStatus());
474  if ($subResponse->hasValue('object')) {
475  $object = $subResponse->getValue('object');
476  if ($object != null) {
477  $response->setValue($object->getOID()->__toString(), $object);
478  }
479  }
480  if ($subResponse->hasValue('list')) {
481  $objects = $subResponse->getValue('list');
482  $response->setValues($objects);
483  }
484  if ($oidStr != null && $subResponse->hasValue($oidStr)) {
485  $object = $subResponse->getValue($oidStr);
486  $response->setValue($oidStr, $object);
487  if ($subResponse->getStatus() == 201) {
488  $this->setLocationHeaderFromOid($oidStr);
489  }
490  }
491  return true;
492  }
493 
494  // in case of error, return default response
495  $response->setErrors($subResponse->getErrors());
496  $response->setStatus(400);
497  return false;
498  }
499 
500  /**
501  * Update oids after commit
502  * @param $event
503  */
504  public function afterCommit(TransactionEvent $event) {
505  if ($event->getPhase() == TransactionEvent::AFTER_COMMIT) {
506  $response = $this->getResponse();
507  $locationOid = $response->getHeader('Location');
508 
509  // replace changed oids
510  $changedOids = $event->getInsertedOids();
511  foreach ($changedOids as $oldOid => $newOid) {
512  if ($response->hasValue($oldOid)) {
513  $value = $response->getValue($oldOid);
514  $response->setValue($newOid, $value);
515  $response->clearValue($oldOid);
516  }
517  if ($locationOid == $oldOid) {
518  $this->setLocationHeaderFromOid($newOid);
519  }
520  }
521  }
522  }
523 
524  /**
525  * Set the location response header according to the given object id
526  * @param $oidStr The serialized object id
527  */
528  protected function setLocationHeaderFromOid($oidStr) {
529  $oid = ObjectId::parse($oidStr);
530  if ($oid) {
531  $response = $this->getResponse();
532  $response->setHeader('Location', $oidStr);
533  }
534  }
535 
536  /**
537  * Get the first oid from the request
538  * @return String
539  */
540  protected function getFirstRequestOid() {
541  $request = $this->getRequest();
542  foreach ($request->getValues() as $key => $value) {
543  if (ObjectId::isValid($key)) {
544  return $key;
545  }
546  }
547  return '';
548  }
549 
550  /**
551  * Get the type that is used in the given role related to the
552  * given source object.
553  * @param $sourceOid ObjectId of the source object
554  * @param $role The role name
555  * @return String
556  */
557  protected function getRelatedType(ObjectId $sourceOid, $role) {
558  $persistenceFacade = $this->getPersistenceFacade();
559  $sourceMapper = $persistenceFacade->getMapper($sourceOid->getType());
560  $relation = $sourceMapper->getRelation($role);
561  return $relation->getOtherType();
562  }
563 }
564 ?>
Session is the interface for session implementations and defines access to session variables.
Definition: Session.php:19
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
executeSubAction($action)
Delegate the current request to another action.
Definition: Controller.php:211
EventManager is responsible for dispatching events to registered listeners.
endTransaction($commit)
End the transaction.
Definition: Controller.php:347
hasErrors()
Check if errors exist.
hasValue($name)
Check for existence of a value.
setLocationHeaderFromOid($oidStr)
Set the location response header according to the given object id.
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration, EventManager $eventManager)
Constructor.
createInRelation()
Create an object of a given type in the given relation.
const AFTER_COMMIT
An AFTER_COMMIT event occurs after the transaction is committed.
TransactionEvent instances are fired at different phases of a transaction.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
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.
updateInRelation()
Update an object in a relation or change the order.
ApplicationError is used to signal errors that occur while processing a request.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:135
static get($code, $data=null)
Factory method for retrieving a predefined error instance.
PersistenceFacade defines the interface for PersistenceFacade implementations.
update()
Update an object or change the order.
getValue($name, $default=null, $validateDesc=null, $suppressException=false)
Get a value.
getType()
Get the type (including namespace)
Definition: ObjectId.php:97
initialize(Request $request, Response $response)
getPhase()
Get the phase at which the event occurred.
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:283
getRelatedType(ObjectId $sourceOid, $role)
Get the type that is used in the given role related to the given source object.
setValue($name, $value)
Set a value.
getRequest()
Get the Request instance.
Definition: Controller.php:251
RESTController handles requests sent from a dstore/Rest client.
readList()
Read objects of a given type.
Application controllers.
Definition: namespaces.php:3
deleteInRelation()
Remove an object from a relation.
Controller is the base class of all controllers.
Definition: Controller.php:49
static getRelationQueryCondition($node, $otherRole)
Get the query condition used to select all related Nodes of a given role.
Definition: NodeUtil.php:117
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
getInsertedOids()
Get the map of oids of inserted objects.
addListener($eventName, $callback)
Register a listener for a given event.
static isValid($oid)
Check if a serialized ObjectId has a valid syntax, the type is known and if the number of primary key...
Definition: ObjectId.php:123
getResponse()
Get the Response instance.
Definition: Controller.php:259
NodeUtil provides services for the Node class.
Definition: NodeUtil.php:28
afterCommit(TransactionEvent $event)
Update oids after commit.
PermissionManager implementations are used to handle all authorization requests.
readInRelation()
Read objects that are related to another object.
getFirstRequestOid()
Get the first oid from the request.
handleSubResponse(Response $subResponse, $oidStr=null)
Create the actual response from the response resulting from delegating to another controller.
getStatus()
Get the response HTTP status code.
create()
Create an object of a given type.
Localization defines the interface for storing localized entity instances and retrieving them back.
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23