CopyController.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 
33 
34 /**
35  * CopyController is used to copy or move Node instances.
36  *
37  * The controller supports the following actions:
38  *
39  * <div class="controller-action">
40  * <div> __Action__ move </div>
41  * <div>
42  * Move the given Node and its children to the given target Node (delete original Node).
43  * | Parameter | Description
44  * |-----------------------|-------------------------
45  * | _in_ `oid` | The object id of the Node to move. The Node and all of its children will be moved
46  * | _in_ `targetOid` | The object id of the parent to attach the moved Node to (if it does not accept the Node type an error occurs) (optional, if empty the new Node has no parent)
47  * | _out_ `oid` | The object id of the newly created Node
48  * </div>
49  * </div>
50  *
51  * <div class="controller-action">
52  * <div> __Action__ copy </div>
53  * <div>
54  * Copy the given Node and its children to the given target Node (keep original Node).
55  * | Parameter | Description
56  * |-----------------------|-------------------------
57  * | _in_ `oid` | The object id of the Node to move. The Node and all of its children will be moved
58  * | _in_ `targetOid` | The object id of the parent to attach the moved Node to (if it does not accept the Node type an error occurs) (optional, if empty the new Node has no parent)
59  * | _in_ `nodesPerCall` | The number of Node instances to copy in one call (default: 50)
60  * | _in_ `recursive` | Boolean whether to copy children too (default: _true_)
61  * </div>
62  * </div>
63  *
64  * For additional actions and parameters see [BatchController actions](@ref BatchController).
65  *
66  * @author ingo herwig <ingo@wemove.com>
67  */
69 
70  // session name constants
71  const OBJECT_MAP_VAR = 'objectmap';
72  const ACTION_VAR = 'action';
73 
74  // persistent iterator id
75  const ITERATOR_ID_VAR = 'CopyController.iteratorid';
76 
77  private $targetNode = null;
78  private $eventManager = null;
79 
80  // default values, maybe overriden by corresponding request values (see above)
81  const NODES_PER_CALL = 50;
82 
83  /**
84  * Constructor
85  * @param $session
86  * @param $persistenceFacade
87  * @param $permissionManager
88  * @param $actionMapper
89  * @param $localization
90  * @param $message
91  * @param $configuration
92  * @param $eventManager
93  */
94  public function __construct(Session $session,
95  PersistenceFacade $persistenceFacade,
96  PermissionManager $permissionManager,
97  ActionMapper $actionMapper,
98  Localization $localization,
99  Message $message,
100  Configuration $configuration,
101  EventManager $eventManager) {
102  parent::__construct($session, $persistenceFacade, $permissionManager,
103  $actionMapper, $localization, $message, $configuration);
104  $this->eventManager = $eventManager;
105  // add transaction listener
106  $this->eventManager->addListener(TransactionEvent::NAME, [$this, 'afterCommit']);
107  }
108 
109  /**
110  * Destructor
111  */
112  public function __destruct() {
113  $this->eventManager->removeListener(TransactionEvent::NAME, [$this, 'afterCommit']);
114  }
115 
116  /**
117  * @see Controller::initialize()
118  */
119  public function initialize(Request $request, Response $response) {
120  // initialize controller
121  if ($request->getAction() != 'continue') {
122  $session = $this->getSession();
123 
124  // set defaults (will be stored with first request)
125  if (!$request->hasValue('nodesPerCall')) {
126  $request->setValue('nodesPerCall', self::NODES_PER_CALL);
127  }
128  if (!$request->hasValue('recursive')) {
129  $request->setValue('recursive', true);
130  }
131 
132  // initialize session variables
133  $this->setLocalSessionValue(self::OBJECT_MAP_VAR, []);
134  $this->setLocalSessionValue(self::ACTION_VAR, $request->getAction());
135 
136  // reset iterator
137  PersistentIterator::reset(self::ITERATOR_ID_VAR, $session);
138  }
139  // initialize parent controller after default request values are set
140  parent::initialize($request, $response);
141  }
142 
143  /**
144  * @see Controller::validate()
145  */
146  protected function validate() {
147  $request = $this->getRequest();
148  $response = $this->getResponse();
149  if ($request->getAction() != 'continue') {
150  // check request values
151  $oid = ObjectId::parse($request->getValue('oid'));
152  if(!$oid) {
153  $response->addError(ApplicationError::get('OID_INVALID',
154  ['invalidOids' => [$request->getValue('oid')]]));
155  return false;
156  }
157  if($request->hasValue('targetoid')) {
158  $targetoid = ObjectId::parse($request->getValue('targetoid'));
159  if(!$targetoid) {
160  $response->addError(ApplicationError::get('OID_INVALID',
161  ['invalidOids' => [$request->getValue('targetoid')]]));
162  return false;
163  }
164  }
165 
166  // check if the parent accepts this node type (only when not adding to root)
167  $addOk = true;
168  if ($request->hasValue('targetoid')) {
169  $persistenceFacade = $this->getPersistenceFacade();
170  $targetOID = ObjectId::parse($request->getValue('targetoid'));
171  $nodeOID = ObjectId::parse($request->getValue('oid'));
172 
173  $targetNode = $this->getTargetNode($targetOID);
174  $nodeType = $nodeOID->getType();
175  $targetType = $targetOID->getType();
176 
177  $tplNode = $persistenceFacade->create($targetType, 1);
178  $possibleChildren = NodeUtil::getPossibleChildren($targetNode, $tplNode);
179  if (!in_array($nodeType, array_keys($possibleChildren))) {
180  $response->addError(ApplicationError::get('ASSOCIATION_INVALID'));
181  $addOk = false;
182  }
183  else {
184  $template = $possibleChildren[$nodeType];
185  if (!$template->getProperty('canCreate')) {
186  $response->addError(ApplicationError::get('ASSOCIATION_INVALID'));
187  $addOk = false;
188  }
189  }
190  }
191  if (!$addOk) {
192  return false;
193  }
194  }
195  // do default validation
196  return parent::validate();
197  }
198 
199  /**
200  * @see BatchController::getWorkPackage()
201  */
202  protected function getWorkPackage($number) {
203  $request = $this->getRequest();
204  $message = $this->getMessage();
205  $name = '';
206  if ($request->getAction() == 'move') {
207  $name = $message->getText('Moving');
208  }
209  else if ($request->getAction() == 'copy') {
210  $name = $message->getText('Copying');
211  }
212  $name .= ': '.$request->getValue('oid');
213 
214  if ($number == 0) {
215  return ['name' => $name, 'size' => 1, 'oids' => [1], 'callback' => 'startProcess'];
216  }
217  else {
218  return null;
219  }
220  }
221 
222  /**
223  * Copy/Move the first node (object ids parameter will be ignored)
224  * @param $oids The object ids to process
225  */
226  protected function startProcess($oids) {
227  $this->requireTransaction();
228  $persistenceFacade = $this->getPersistenceFacade();
229  $session = $this->getSession();
230 
231  // restore the request from session
232  $action = $this->getLocalSessionValue(self::ACTION_VAR);
233  $targetOID = ObjectId::parse($this->getRequestValue('targetoid'));
234  $nodeOID = ObjectId::parse($this->getRequestValue('oid'));
235 
236  // do the action
237  if ($action == 'move') {
238  $persistenceFacade = $this->getPersistenceFacade();
239  // with move action, we only need to attach the Node to the new target
240  // the children will not be loaded, they will be moved automatically
241  $nodeCopy = $persistenceFacade->load($nodeOID);
242  if ($nodeCopy) {
243  if ($targetOID != null) {
244  // attach the node to the target node
245  $parentNode = $this->getTargetNode($targetOID);
246  $parentNode->addNode($nodeCopy);
247  }
248 
249  $this->modify($nodeCopy);
250 
251  // set the result and finish
252  $this->endProcess($nodeCopy->getOID());
253 
254  $logger = $this->getLogger();
255  if ($logger->isInfoEnabled()) {
256  $logger->info("Moved: ".$nodeOID." to ".$parentNode->getOID());
257  }
258  }
259  }
260  else if ($action == 'copy') {
261  // with copy action, we need to attach a copy of the Node to the new target,
262  // the children need to be loaded and treated in the same way too
263  $iterator = new PersistentIterator(self::ITERATOR_ID_VAR, $persistenceFacade,
264  $session, $nodeOID, ['composite']);
265  $iterator->save();
266 
267  // copy the first node in order to reduce the number of calls for a single copy
268  $nodeCopy = $this->copyNode($iterator->current());
269  if ($nodeCopy) {
270  if ($targetOID != null) {
271  // attach the copy to the target node
272  $parentNode = $this->getTargetNode($targetOID);
273  $parentNode->addNode($nodeCopy);
274  }
275 
276  $this->modify($nodeCopy);
277 
278  $iterator->next();
279 
280  // proceed if nodes are left
281  if (StringUtil::getBoolean($this->getRequestValue('recursive')) && $iterator->valid()) {
282  $iterator->save();
283 
284  $name = $this->getMessage()->getText('Copying tree: continue with %0%', [$iterator->current()]);
285  $this->addWorkPackage($name, 1, [null], 'copyNodes');
286  }
287  else {
288  // set the result and finish
289  $this->endProcess($nodeCopy->getOID());
290  }
291  }
292  }
293  }
294 
295  /**
296  * Copy nodes provided by the persisted iterator (object ids parameter will be ignored)
297  * @param $oids The object ids to process
298  */
299  protected function copyNodes($oids) {
300  $this->requireTransaction();
301  $persistenceFacade = $this->getPersistenceFacade();
302  $session = $this->getSession();
303 
304  // restore the request from session
305  $nodeOID = ObjectId::parse($this->getRequestValue('oid'));
306 
307  // check for iterator in session
308  $iterator = PersistentIterator::load(self::ITERATOR_ID_VAR, $persistenceFacade, $session);
309 
310  // no iterator, finish
311  if ($iterator == null || !$iterator->valid()) {
312  // set the result and finish
313  $this->endProcess($this->getCopyOID($nodeOID));
314  }
315 
316  // process nodes
317  $counter = 0;
318  while ($iterator->valid() && $counter < $this->getRequestValue('nodesPerCall')) {
319  $currentOID = $iterator->current();
320  $this->copyNode($currentOID);
321 
322  $iterator->next();
323  $counter++;
324  }
325 
326  // decide what to do next
327  if ($iterator->valid()) {
328  // proceed with current iterator
329  $iterator->save();
330 
331  $name = $this->getMessage()->getText('Copying tree: continue with %0%', [$iterator->current()]);
332  $this->addWorkPackage($name, 1, [null], 'copyNodes');
333  }
334  else {
335  // set the result and finish
336  $this->endProcess($this->getCopyOID($nodeOID));
337  }
338  }
339 
340  /**
341  * Finish the process and set the result
342  * @param $oid The object id of the newly created Node
343  */
344  protected function endProcess(ObjectId $oid) {
345  $response = $this->getResponse();
346  $response->setValue('oid', $oid);
347  }
348 
349  /**
350  * Create a copy of the node with the given object id. The returned
351  * node is already persisted.
352  * @param $oid The object id of the node to copy
353  * @return The copied Node or null
354  */
355  protected function copyNode(ObjectId $oid) {
356  $logger = $this->getLogger();
357  if ($logger->isDebugEnabled()) {
358  $logger->debug("Copying node ".$oid);
359  }
360  $persistenceFacade = $this->getPersistenceFacade();
361 
362  // load the original node
363  $node = $persistenceFacade->load($oid);
364  if ($node == null) {
365  throw new PersistenceException("Can't load node '".$oid."'");
366  }
367 
368  // check if we already have a copy of the node
369  $nodeCopy = $this->getCopy($node->getOID());
370  if ($nodeCopy == null) {
371  // if not, create it
372  $nodeCopy = $persistenceFacade->create($node->getType());
373  $node->copyValues($nodeCopy, false);
374  }
375 
376  // save copy
377  $this->registerCopy($node, $nodeCopy);
378 
379  if ($logger->isInfoEnabled()) {
380  $logger->info("Copied: ".$node->getOID()." to ".$nodeCopy->getOID());
381  }
382  if ($logger->isDebugEnabled()) {
383  $logger->debug($nodeCopy->__toString());
384  }
385 
386  // create the connections to already copied relatives
387  // this must be done after saving the node in order to have a correct oid
388  $mapper = $node->getMapper();
389  $relations = $mapper->getRelations();
390  foreach ($relations as $relation) {
391  if ($relation->getOtherNavigability()) {
392  $otherRole = $relation->getOtherRole();
393  $relativeValue = $node->getValue($otherRole);
394  $relatives = $relation->isMultiValued() ? $relativeValue :
395  ($relativeValue != null ? [$relativeValue] : []);
396  foreach ($relatives as $relative) {
397  $copiedRelative = $this->getCopy($relative->getOID());
398  if ($copiedRelative != null) {
399  $nodeCopy->addNode($copiedRelative, $otherRole);
400  if ($logger->isDebugEnabled()) {
401  $logger->debug("Added ".$copiedRelative->getOID()." to ".$nodeCopy->getOID());
402  $logger->debug($copiedRelative->__toString());
403  }
404  }
405  }
406  }
407  }
408  return $nodeCopy;
409  }
410 
411  /**
412  * Update oids after commit
413  * @param $event
414  */
415  public function afterCommit(TransactionEvent $event) {
416  if ($event->getPhase() == TransactionEvent::AFTER_COMMIT) {
417  $changedOids = $event->getInsertedOids();
418  $this->updateCopyOIDs($changedOids);
419  }
420  }
421 
422  /**
423  * Get the target node from the request parameter targetoid
424  * @param $targetOID The object id of the target node
425  * @return Node instance
426  */
427  protected function getTargetNode(ObjectId $targetOID) {
428  if ($this->targetNode == null) {
429  // load parent node
430  $persistenceFacade = $this->getPersistenceFacade();
431  $targetNode = $persistenceFacade->load($targetOID);
432  $this->targetNode = $targetNode;
433  }
434  return $this->targetNode;
435  }
436 
437  /**
438  * Register a copied node in the session for later reference
439  * @param $origNode The original Node instance
440  * @param $copyNode The copied Node instance
441  */
442  protected function registerCopy(PersistentObject $origNode, PersistentObject $copyNode) {
443  // store oid in the registry
444  $registry = $this->getLocalSessionValue(self::OBJECT_MAP_VAR);
445  $registry[$origNode->getOID()->__toString()] = $copyNode->getOID()->__toString();
446  $this->setLocalSessionValue(self::OBJECT_MAP_VAR, $registry);
447  }
448 
449  /**
450  * Update the copied object ids in the registry
451  * @param $oidMap Map of changed object ids (key: old value, value: new value)
452  */
453  protected function updateCopyOIDs(array $oidMap) {
454  $registry = $this->getLocalSessionValue(self::OBJECT_MAP_VAR);
455  // registry maybe deleted already if it's the last step
456  if ($registry) {
457  $flippedRegistry = array_flip($registry);
458  foreach ($oidMap as $oldOid => $newOid) {
459  if (isset($flippedRegistry[$oldOid])) {
460  $key = $flippedRegistry[$oldOid];
461  unset($flippedRegistry[$oldOid]);
462  $flippedRegistry[$newOid] = $key;
463  }
464  }
465  $registry = array_flip($flippedRegistry);
466  $this->setLocalSessionValue(self::OBJECT_MAP_VAR, $registry);
467  }
468  }
469 
470  /**
471  * Get the object id of the copied node for a node id
472  * @param $origOID The object id of the original node
473  * @return ObjectId or null, if it does not exist already
474  */
475  protected function getCopyOID(ObjectId $origOID) {
476  $registry = $this->getLocalSessionValue(self::OBJECT_MAP_VAR);
477 
478  // check if the oid exists in the registry
479  $oidStr = $origOID->__toString();
480  if (!isset($registry[$oidStr])) {
481  $logger = $this->getLogger();
482  if ($logger->isDebugEnabled()) {
483  $logger->debug("Copy of ".$oidStr." not found.");
484  }
485  return null;
486  }
487 
488  $copyOID = ObjectId::parse($registry[$oidStr]);
489  return $copyOID;
490  }
491 
492  /**
493  * Get the copied node for a node id
494  * @param $origOID The object id of the original node
495  * @return Copied Node or null, if it does not exist already
496  */
497  protected function getCopy(ObjectId $origOID) {
498  $copyOID = $this->getCopyOID($origOID);
499  if ($copyOID != null) {
500  $persistenceFacade = $this->getPersistenceFacade();
501  $nodeCopy = $persistenceFacade->load($copyOID);
502  return $nodeCopy;
503  }
504  else {
505  return null;
506  }
507  }
508 
509  /**
510  * Modify the given Node before save action (Called only for the copied root Node, not for its children)
511  * @note Subclasses will override this to implement special application requirements.
512  * @param $node The Node instance to modify.
513  */
514  protected function modify(PersistentObject $node) {}
515 }
516 ?>
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
EventManager is responsible for dispatching events to registered listeners.
copyNode(ObjectId $oid)
Create a copy of the node with the given object id.
PersistenceException signals an exception in the persistence service.
getCopyOID(ObjectId $origOID)
Get the object id of the copied node for a node id.
static getBoolean($string)
Get the boolean value of a string.
Definition: StringUtil.php:405
getOID()
Get the object id of the PersistentObject.
endProcess(ObjectId $oid)
Finish the process and set the result.
registerCopy(PersistentObject $origNode, PersistentObject $copyNode)
Register a copied node in the session for later reference.
afterCommit(TransactionEvent $event)
Update oids after commit.
__toString()
Get a string representation of the object id.
Definition: ObjectId.php:215
PersistentIterator is used to iterate over a tree/list built of persistent objects using a Depth-Firs...
getLocalSessionValue($key, $default=null)
Set the value of a local session variable.
Definition: Controller.php:443
hasValue($name)
Check for existence of a value.
StringUtil provides support for string manipulation.
Definition: StringUtil.php:18
const AFTER_COMMIT
An AFTER_COMMIT event occurs after the transaction is committed.
TransactionEvent instances are fired at different phases of a transaction.
static reset($id, Session $session)
Reset the iterator with the given id.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
getAction()
Get the name of the action.
initialize(Request $request, Response $response)
updateCopyOIDs(array $oidMap)
Update the copied object ids in the registry.
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
setLocalSessionValue($key, $value)
Get the value of a local session variable.
Definition: Controller.php:454
copyNodes($oids)
Copy nodes provided by the persisted iterator (object ids parameter will be ignored)
ApplicationError is used to signal errors that occur while processing a request.
CopyController is used to copy or move Node instances.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:135
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration, EventManager $eventManager)
Constructor.
static get($code, $data=null)
Factory method for retrieving a predefined error instance.
PersistenceFacade defines the interface for PersistenceFacade implementations.
getRequestValue($name)
Get a value from the initial request.
getPhase()
Get the phase at which the event occurred.
modify(PersistentObject $node)
Modify the given Node before save action (Called only for the copied root Node, not for its children)
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:283
setValue($name, $value)
Set a value.
getRequest()
Get the Request instance.
Definition: Controller.php:251
Node adds the concept of relations to PersistentObject.
Definition: Node.php:34
Application controllers.
Definition: namespaces.php:3
BatchController is used to process complex, longer running actions, that need to be divided into seve...
addWorkPackage($name, $size, array $oids, $callback, $args=null)
Add a work package to session.
static load($id, $persistenceFacade, $session)
Load an iterator state from the session.
PersistentObject defines the interface of all persistent objects.
startProcess($oids)
Copy/Move the first node (object ids parameter will be ignored)
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.
getResponse()
Get the Response instance.
Definition: Controller.php:259
NodeUtil provides services for the Node class.
Definition: NodeUtil.php:28
PermissionManager implementations are used to handle all authorization requests.
getTargetNode(ObjectId $targetOID)
Get the target node from the request parameter targetoid.
getSession()
Get the Session instance.
Definition: Controller.php:275
Localization defines the interface for storing localized entity instances and retrieving them back.
getCopy(ObjectId $origOID)
Get the copied node for a node id.
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23