XMLExportController.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 
29 
30 /**
31  * XMLExportController exports the content tree into an XML file.
32  *
33  * The controller supports the following actions:
34  *
35  * <div class="controller-action">
36  * <div> __Action__ _default_ </div>
37  * <div>
38  * Initiate the export.
39  * | Parameter | Description
40  * |------------------------|-------------------------
41  * | _in_ `docFile` | The name of the file to write to (path relative to script main location) (default: 'export.xml')
42  * | _in_ `docType` | The document type (will be written into XML header) (default: '')
43  * | _in_ `dtd` | The dtd (will be written into XML header) (default: '')
44  * | _in_ `docRootElement` | The root element of the document (use this to enclose different root types if necessary) (default: 'Root')
45  * | _in_ `docLinebreak` | The linebreak character(s) to use (default: '\n')
46  * | _in_ `docIndent` | The indent character(s) to use (default: ' ')
47  * | _in_ `nodesPerCall` | The number of nodes to process in one call (default: 10)
48  * </div>
49  * </div>
50  *
51  * For additional actions and parameters see [BatchController actions](@ref BatchController).
52  *
53  * @author ingo herwig <ingo@wemove.com>
54  */
56  const CACHE_KEY_ROOT_OIDS = 'rootOids';
57  const CACHE_KEY_EXPORTED_OIDS = 'exportedOids';
58  const CACHE_KEY_LAST_INDENT = 'lastIndent';
59  const CACHE_KEY_TAGS_TO_CLOSE = 'tagsToClose';
60 
61  // persistent iterator id
62  const ITERATOR_ID_VAR = 'XMLExportController.iteratorid';
63 
64  // default values, maybe overriden by corresponding request values (see above)
65  const DOCFILE = "export.xml";
66  const DOCTYPE = "";
67  const DTD = "";
68  const DOCROOTELEMENT = "Root";
69  const DOCLINEBREAK = "\n";
70  const DOCINDENT = " ";
71  const NODES_PER_CALL = 10;
72 
73  private $cache = null;
74  private $fileUtil = null;
75 
76  /**
77  * Constructor
78  * @param $session
79  * @param $persistenceFacade
80  * @param $permissionManager
81  * @param $actionMapper
82  * @param $localization
83  * @param $message
84  * @param $configuration
85  * @param $staticCache
86  */
87  public function __construct(Session $session,
88  PersistenceFacade $persistenceFacade,
89  PermissionManager $permissionManager,
90  ActionMapper $actionMapper,
91  Localization $localization,
92  Message $message,
93  Configuration $configuration,
94  Cache $staticCache) {
95  parent::__construct($session, $persistenceFacade, $permissionManager,
96  $actionMapper, $localization, $message, $configuration);
97  $this->cache = $staticCache;
98  $this->fileUtil = new FileUtil();
99  }
100 
101  /**
102  * @see Controller::initialize()
103  */
104  public function initialize(Request $request, Response $response) {
105  // initialize controller
106  if ($request->getAction() != 'continue') {
107  $session = $this->getSession();
108 
109  // set defaults (will be stored with first request)
110  if (!$request->hasValue('docFile')) {
111  $request->setValue('docFile', self::DOCFILE);
112  }
113  if (!$request->hasValue('docType')) {
114  $request->setValue('docType', self::DOCTYPE);
115  }
116  if (!$request->hasValue('dtd')) {
117  $request->setValue('dtd', self::DTD);
118  }
119  if (!$request->hasValue('docRootElement')) {
120  $request->setValue('docRootElement', self::DOCROOTELEMENT);
121  }
122  if (!$request->hasValue('docLinebreak')) {
123  $request->setValue('docLinebreak', self::DOCLINEBREAK);
124  }
125  if (!$request->hasValue('docIndent')) {
126  $request->setValue('docIndent', self::DOCINDENT);
127  }
128  if (!$request->hasValue('nodesPerCall')) {
129  $request->setValue('nodesPerCall', self::NODES_PER_CALL);
130  }
131 
132  // set the cache section and directory for the download file
133  $config = $this->getConfiguration();
134  $cacheBaseDir = WCMF_BASE.$config->getValue('cacheDir', 'StaticCache');
135  $cacheSection = 'xml-export-'.uniqid().'/cache';
136  $downloadDir = $cacheBaseDir.dirname($cacheSection).'/';
137  FileUtil::mkdirRec($downloadDir);
138  $request->setValue('cacheSection', $cacheSection);
139  $request->setValue('downloadFile', $downloadDir.$request->getValue('docFile'));
140 
141  // initialize cache
142  $this->cache->put($cacheSection, self::CACHE_KEY_LAST_INDENT, 0);
143  $this->cache->put($cacheSection, self::CACHE_KEY_TAGS_TO_CLOSE, []);
144 
145  // reset iterator
146  PersistentIterator::reset($this->ITERATOR_ID_VAR, $session);
147  }
148  // initialize parent controller after default request values are set
149  parent::initialize($request, $response);
150  }
151 
152  /**
153  * @see BatchController::getWorkPackage()
154  */
155  protected function getWorkPackage($number) {
156  if ($number == 0) {
157  return ['name' => $this->getMessage()->getText('Initialization'),
158  'size' => 1, 'oids' => [1], 'callback' => 'initExport'];
159  }
160  else {
161  return null;
162  }
163  }
164 
165  /**
166  * @see BatchController::getDownloadFile()
167  */
168  protected function getDownloadFile() {
169  return $this->getRequestValue('downloadFile');
170  }
171 
172  /**
173  * Initialize the XML export (object ids parameter will be ignored)
174  * @param $oids The object ids to process
175  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
176  */
177  protected function initExport($oids) {
178  // get document definition
179  $docFile = $this->getDownloadFile();
180  $docType = $this->getRequestValue('docType');
181  $dtd = $this->getRequestValue('dtd');
182  $docRootElement = $this->getRequestValue('docRootElement');
183  $docLinebreak = $this->getRequestValue('docLinebreak');
184  $cacheSection = $this->getRequestValue('cacheSection');
185 
186  // delete export file
187  if (file_exists($docFile)) {
188  unlink($docFile);
189  }
190 
191  // start document
192  $fileHandle = fopen($docFile, "a");
193  $this->fileUtil->fputsUnicode($fileHandle, '<?xml version="1.0" encoding="UTF-8"?>'.$docLinebreak);
194  if ($docType != "") {
195  $this->fileUtil->fputsUnicode($fileHandle, '<!DOCTYPE '.$docType.' SYSTEM "'.$dtd.'">'.$docLinebreak);
196  }
197  $this->fileUtil->fputsUnicode($fileHandle, '<'.$docRootElement.'>'.$docLinebreak);
198  fclose($fileHandle);
199 
200  // get root types from ini file
201  $rootOIDs = [];
202  $config = $this->getConfiguration();
203  $rootTypes = $config->getValue('rootTypes', 'application');
204  if (is_array($rootTypes)) {
205  $persistenceFacade = $this->getPersistenceFacade();
206  foreach($rootTypes as $rootType) {
207  $rootOIDs = array_merge($rootOIDs, $persistenceFacade->getOIDs($rootType));
208  }
209  }
210 
211  // store root object ids in cache
212  $nextOID = array_shift($rootOIDs);
213  $this->cache->put($cacheSection, self::CACHE_KEY_ROOT_OIDS, $rootOIDs);
214 
215  // empty exported oids
216  $tmp = [];
217  $this->cache->put($cacheSection, self::CACHE_KEY_EXPORTED_OIDS, $tmp);
218 
219  // create work package for first root node
220  $this->addWorkPackage(
221  $this->getMessage()->getText('Exporting tree: start with %0%', [$nextOID]),
222  1, [$nextOID], 'exportNodes');
223  }
224 
225  /**
226  * Serialize all Nodes with given object ids to XML
227  * @param $oids The object ids to process
228  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
229  */
230  protected function exportNodes($oids) {
231  // Export starts from root oids and iterates over all children.
232  // On every call we have to decide what to do:
233  // - If there is an iterator stored in the session we are inside a tree and continue iterating (_NODES_PER_CALL nodes)
234  // until the iterator finishes
235  // - If the oids array holds one value!=null this is assumed to be an root oid and a new iterator is constructed
236  // - If there is no iterator and no oid given, we return
237 
238  $session = $this->getSession();
239  $persistenceFacade = $this->getPersistenceFacade();
240  $message = $this->getMessage();
241 
242  // get document definition
243  $docFile = $this->getDownloadFile();
244  $nodesPerCall = $this->getRequestValue('nodesPerCall');
245  $cacheSection = $this->getRequestValue('cacheSection');
246 
247  // check for iterator in session
248  $iterator = PersistentIterator::load($this->ITERATOR_ID_VAR, $persistenceFacade, $session);
249  // no iterator but oid given, start with new root oid
250  if ($iterator == null && sizeof($oids) > 0 && $oids[0] != null) {
251  $iterator = new PersistentIterator($this->ITERATOR_ID_VAR, $persistenceFacade, $session, $oids[0]);
252  }
253  // no iterator, no oid, finish
254  if ($iterator == null) {
255  $this->addWorkPackage($message->getText('Finish'), 1, [null], 'finishExport');
256  return;
257  }
258 
259  // process nodes
260  $fileHandle = fopen($docFile, "a");
261  $counter = 0;
262  while ($iterator->valid() && $counter < $nodesPerCall) {
263  // write node
264  $this->writeNode($fileHandle, $iterator->current(), $iterator->key()+1);
265  $iterator->next();
266  $counter++;
267  }
268  $this->endTags($fileHandle, 0);
269  fclose($fileHandle);
270 
271  // decide what to do next
272  $rootOIDs = $this->cache->get($cacheSection, self::CACHE_KEY_ROOT_OIDS);
273  if (!$iterator->valid() && sizeof($rootOIDs) > 0) {
274  // if the current iterator is finished, reset the iterator and proceed with the next root oid
275  $nextOID = array_shift($rootOIDs);
276  // store remaining root oids in the cache
277  $this->cache->put($cacheSection, self::CACHE_KEY_ROOT_OIDS, $rootOIDs);
278  // delete iterator to start with new root oid
279  PersistentIterator::reset($this->ITERATOR_ID_VAR, $session);
280 
281  $name = $message->getText('Exporting tree: start with %0%', [$nextOID]);
282  $this->addWorkPackage($name, 1, [$nextOID], 'exportNodes');
283  }
284  elseif ($iterator->valid()) {
285  // proceed with current iterator
286  $iterator->save();
287 
288  $name = $message->getText('Exporting tree: continue with %0%', [$iterator->current()]);
289  $this->addWorkPackage($name, 1, [null], 'exportNodes');
290  }
291  else {
292  // finish
293  $this->addWorkPackage($message->getText('Finish'), 1, [null], 'finishExport');
294  }
295  }
296 
297  /**
298  * Finish the XML export (object ids parameter will be ignored)
299  * @param $oids The object ids to process
300  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
301  */
302  protected function finishExport($oids) {
303  // get document definition
304  $docFile = $this->getDownloadFile();
305  $docRootElement = $this->getRequestValue('docRootElement');
306  $docLinebreak = $this->getRequestValue('docLinebreak');
307  $cacheSection = $this->getRequestValue('cacheSection');
308 
309  // end document
310  $fileHandle = fopen($docFile, "a");
311  $this->endTags($fileHandle, 0);
312  $this->fileUtil->fputsUnicode($fileHandle, '</'.$docRootElement.'>'.$docLinebreak);
313  fclose($fileHandle);
314 
315  // clear cache
316  $tmp = null;
317  $this->cache->put($cacheSection, self::CACHE_KEY_ROOT_OIDS, $tmp);
318  }
319 
320  /**
321  * Ends all tags up to $curIndent level
322  * @param $fileHandle The file handle to write to
323  * @param $curIndent The depth of the node in the tree
324  */
325  protected function endTags($fileHandle, $curIndent) {
326  // get document definition
327  $docIndent = $this->getRequestValue('docIndent');
328  $docLinebreak = $this->getRequestValue('docLinebreak');
329  $cacheSection = $this->getRequestValue('cacheSection');
330 
331  // get document state from cache
332  $lastIndent = $this->cache->get($cacheSection, self::CACHE_KEY_LAST_INDENT);
333  $tagsToClose = $this->cache->get($cacheSection, self::CACHE_KEY_TAGS_TO_CLOSE);
334 
335  // write last opened and not closed tags
336  if ($curIndent < $lastIndent) {
337  for ($i=$lastIndent-$curIndent; $i>0; $i--) {
338  $closeTag = array_shift($tagsToClose);
339  if ($closeTag) {
340  $this->fileUtil->fputsUnicode($fileHandle, str_repeat($docIndent, $closeTag["indent"]).'</'.$closeTag["name"].'>'.$docLinebreak);
341  }
342  }
343  }
344 
345  // update document state in cache
346  $this->cache->put($cacheSection, self::CACHE_KEY_LAST_INDENT, $lastIndent);
347  $this->cache->put($cacheSection, self::CACHE_KEY_TAGS_TO_CLOSE, $tagsToClose);
348  }
349 
350  /**
351  * Serialize a Node to XML
352  * @param $fileHandle The file handle to write to
353  * @param $oid The object id of the node
354  * @param $depth The depth of the node in the tree
355  */
356  protected function writeNode($fileHandle, ObjectId $oid, $depth) {
357  $persistenceFacade = $this->getPersistenceFacade();
358 
359  // get document definition
360  $docIndent = $this->getRequestValue('docIndent');
361  $docLinebreak = $this->getRequestValue('docLinebreak');
362  $cacheSection = $this->getRequestValue('cacheSection');
363 
364  // get document state from cache
365  $lastIndent = $this->cache->get($cacheSection, self::CACHE_KEY_LAST_INDENT);
366  $tagsToClose = $this->cache->get($cacheSection, self::CACHE_KEY_TAGS_TO_CLOSE);
367 
368  // load node and get element name
369  $node = $persistenceFacade->load($oid);
370  $elementName = $persistenceFacade->getSimpleType($node->getType());
371 
372  // check if the node is written already
373  $exportedOids = $this->cache->get($cacheSection, self::CACHE_KEY_EXPORTED_OIDS);
374  if (!in_array($oid->__toString(), $exportedOids)) {
375  // write node
376  $mapper = $node->getMapper();
377 
378  $hasUnvisitedChildren = $this->getNumUnvisitedChildren($node) > 0;
379 
380  $curIndent = $depth;
381  $this->endTags($fileHandle, $curIndent);
382 
383  // write object's content
384  // open start tag
385  $this->fileUtil->fputsUnicode($fileHandle, str_repeat($docIndent, $curIndent).'<'.$elementName);
386  // write object attributes
387  $attributes = $mapper->getAttributes();
388  foreach ($attributes as $curAttribute) {
389  $attributeName = $curAttribute->getName();
390  $value = $node->getValue($attributeName);
391  $this->fileUtil->fputsUnicode($fileHandle, ' '.$attributeName.'="'.$this->formatValue($value).'"');
392  }
393  // close start tag
394  $this->fileUtil->fputsUnicode($fileHandle, '>');
395  if ($hasUnvisitedChildren) {
396  $this->fileUtil->fputsUnicode($fileHandle, $docLinebreak);
397  }
398 
399  // remember end tag if not closed
400  if ($hasUnvisitedChildren) {
401  $closeTag = ["name" => $elementName, "indent" => $curIndent];
402  array_unshift($tagsToClose, $closeTag);
403  }
404  else {
405  $this->fileUtil->fputsUnicode($fileHandle, '</'.$elementName.'>'.$docLinebreak);
406  }
407  // remember current indent
408  $lastIndent = $curIndent;
409 
410  // register exported node
411  $exportedOids[] = $oid->__toString();
412  $this->cache->put($cacheSection, self::CACHE_KEY_EXPORTED_OIDS, $exportedOids);
413  }
414 
415  // update document state in cache
416  $this->cache->put($cacheSection, self::CACHE_KEY_LAST_INDENT, $lastIndent);
417  $this->cache->put($cacheSection, self::CACHE_KEY_TAGS_TO_CLOSE, $tagsToClose);
418  }
419 
420  /**
421  * Get number of children of the given node, that were not visited yet
422  * @param $node
423  * @return Integer
424  */
425  protected function getNumUnvisitedChildren(Node $node) {
426  $cacheSection = $this->getRequestValue('cacheSection');
427  $exportedOids = $this->cache->get($cacheSection, self::CACHE_KEY_EXPORTED_OIDS);
428 
429  $childOIDs = [];
430  $mapper = $node->getMapper();
431  $relations = $mapper->getRelations('child');
432  foreach ($relations as $relation) {
433  if ($relation->getOtherNavigability()) {
434  $childValue = $node->getValue($relation->getOtherRole());
435  if ($childValue != null) {
436  $children = $relation->isMultiValued() ? $childValue : [$childValue];
437  foreach ($children as $child) {
438  $childOIDs[] = $child->getOID();
439  }
440  }
441  }
442  }
443  $numUnvisitedChildren = 0;
444  foreach ($childOIDs as $childOid) {
445  if (!in_array($childOid->__toString(), $exportedOids)) {
446  $numUnvisitedChildren++;
447  }
448  }
449  return $numUnvisitedChildren;
450  }
451 
452  /**
453  * Format a value for XML output
454  * @param $value The value to format
455  * @return The formatted value
456  * @note Subclasses may overrite this for special application requirements
457  */
458  protected function formatValue($value) {
459  return htmlentities(str_replace(["\r", "\n"], ["", ""], nl2br($value)), ENT_QUOTES);
460  }
461 
462  /**
463  * @see BatchController::cleanup()
464  */
465  protected function cleanup() {
466  $downloadDir = dirname($this->getRequestValue('downloadFile'));
467  FileUtil::emptyDir($downloadDir);
468  rmdir($downloadDir);
469  parent::cleanup();
470  }
471 }
472 ?>
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
formatValue($value)
Format a value for XML output.
static mkdirRec($dirname, $perm=0775)
Recursive directory creation.
Definition: FileUtil.php:215
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
XMLExportController exports the content tree into an XML file.
endTags($fileHandle, $curIndent)
Ends all tags up to $curIndent level.
writeNode($fileHandle, ObjectId $oid, $depth)
Serialize a Node to XML.
getValue($name)
Definition: Node.php:97
__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...
hasValue($name)
Check for existence of a value.
exportNodes($oids)
Serialize all Nodes with given object ids to XML.
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.
Implementations of Configuration give access to the application configuration.
getMessage()
Get the Message instance.
Definition: Controller.php:315
PersistenceFacade defines the interface for PersistenceFacade implementations.
getRequestValue($name)
Get a value from the initial request.
static emptyDir($dirname)
Empty a directory.
Definition: FileUtil.php:225
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:283
setValue($name, $value)
Set a value.
FileUtil provides basic support for file functionality like HTTP file upload.
Definition: FileUtil.php:22
Node adds the concept of relations to PersistentObject.
Definition: Node.php:34
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration, Cache $staticCache)
Constructor.
Application controllers.
Definition: namespaces.php:3
initialize(Request $request, Response $response)
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.
Controller is the base class of all controllers.
Definition: Controller.php:49
initExport($oids)
Initialize the XML export (object ids parameter will be ignored)
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
PermissionManager implementations are used to handle all authorization requests.
getSession()
Get the Session instance.
Definition: Controller.php:275
getNumUnvisitedChildren(Node $node)
Get number of children of the given node, that were not visited yet.
finishExport($oids)
Finish the XML export (object ids parameter will be ignored)
Cache defines the interface for cache implementations.
Definition: Cache.php:21
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