CSVExportController.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 
24 use wcmf\lib\util\StringUtil;
25 
26 /**
27  * CSVExportController exports instances of one type into a CSV file. It uses
28  * the `fputcsv` function of PHP with the default values for delimiter, enclosing
29  * and escape character.
30  *
31  * The controller supports the following actions:
32  *
33  * <div class="controller-action">
34  * <div> __Action__ _default_ </div>
35  * <div>
36  * Initiate the export.
37  * | Parameter | Description
38  * |------------------------|-------------------------
39  * | _in_ `docFile` | The name of the file to write to (path relative to script main location) (default: 'export.csv')
40  * | _in_ `className` | The entity type to export instances of or the source type, when exporting a relation (see sourceId, relation)
41  * | _in_ `sortFieldName` | The field name to sort the list by. Must be one of the fields of the type selected by the className parameter. If omitted, the sorting is undefined (optional)
42  * | _in_ `sortDirection` | The direction to sort the list. Must be either _asc_ for ascending or _desc_ for descending (optional, default: _asc_)
43  * | _in_ `query` | A query condition encoded in RQL to be used with StringQuery::setRQLConditionString()
44  * | _in_ `translateValues` | Boolean whether list values should be translated to their display values (optional, default: _true_)
45  * | _in_ `sourceId` | When exporting a relation: Id of the object to which the exported objects are related (determines the object id together with _className_)
46  * | _in_ `relation` | When exporting a relation: Name of the relation to the object defined by _sourceId_ (determines the type of the returned objects)
47  * | _in_ `nodesPerCall` | The number of nodes to process in one call (default: 50)
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 
57  // default values, maybe overriden by corresponding request values (see above)
58  const DOCFILE = "export.csv";
59  const NODES_PER_CALL = 50;
60 
61  /**
62  * @see Controller::initialize()
63  */
64  public function initialize(Request $request, Response $response) {
65  // initialize controller
66  if ($request->getAction() != 'continue') {
67  // set defaults (will be stored with first request)
68  if (!$request->hasValue('docFile')) {
69  $request->setValue('docFile', self::DOCFILE);
70  }
71  if (!$request->hasValue('translateValues')) {
72  $request->setValue('translateValues', true);
73  }
74  if (!$request->hasValue('nodesPerCall')) {
75  $request->setValue('nodesPerCall', self::NODES_PER_CALL);
76  }
77 
78  // set the cache section and directory for the download file
79  $config = $this->getConfiguration();
80  $cacheBaseDir = WCMF_BASE.$config->getValue('cacheDir', 'StaticCache');
81  $cacheSection = 'csv-export-'.uniqid().'/cache';
82  $downloadDir = $cacheBaseDir.dirname($cacheSection).'/';
83  FileUtil::mkdirRec($downloadDir);
84  $request->setValue('cacheSection', $cacheSection);
85  $request->setValue('downloadFile', $downloadDir.$request->getValue('docFile'));
86  }
87  // initialize parent controller after default request values are set
88  parent::initialize($request, $response);
89  }
90 
91  /**
92  * @see Controller::validate()
93  */
94  protected function validate() {
95  $request = $this->getRequest();
96  $response = $this->getResponse();
97  if ($request->getAction() != 'continue') {
98  if (!$request->hasValue('className') ||
99  !$this->getPersistenceFacade()->isKnownType($request->getValue('className')))
100  {
101  $response->addError(ApplicationError::get('PARAMETER_INVALID',
102  ['invalidParameters' => ['className']]));
103  return false;
104  }
105  // check for permission to read instances of className
106  $permissionManager = $this->getPermissionManager();
107  if (!$permissionManager->authorize($request->getValue('className'), '', PersistenceAction::READ)) {
108  $response->addError(ApplicationError::get('PERMISSION_DENIED'));
109  return false;
110  }
111  // check permission to read sourceOid and relation instances
112  if ($request->hasValue('className') && $request->hasValue('sourceId')) {
113  $sourceOid = new ObjectId($request->getValue('className'), $request->getValue('sourceId'));
114  if (!$permissionManager->authorize($sourceOid, '', PersistenceAction::READ)) {
115  $response->addError(ApplicationError::get('PERMISSION_DENIED'));
116  return false;
117  }
118  if ($request->hasValue('relation')) {
119  $persistenceFacade = $this->getPersistenceFacade();
120  $sourceMapper = $persistenceFacade->getMapper($sourceOid->getType());
121  $relation = $sourceMapper->getRelation($request->getValue('relation'));
122  if (!$permissionManager->authorize($relation->getOtherType(), '', PersistenceAction::READ)) {
123  $response->addError(ApplicationError::get('PERMISSION_DENIED'));
124  return false;
125  }
126  }
127  else {
128  $response->addError(ApplicationError::get('PARAMETER_MISSING',
129  ['missingParameters' => ['relation']]));
130  return false;
131  }
132  }
133  if ($request->hasValue('sortFieldName') &&
134  !$this->getPersistenceFacade()->getMapper($request->getValue('className'))->hasAttribute($request->hasValue('sortFieldName'))) {
135  $response->addError(ApplicationError::get('SORT_FIELD_UNKNOWN'));
136  return false;
137  }
138  if ($request->hasValue('sortDirection')) {
139  $sortDirection = $request->getValue('sortDirection');
140  if (strtolower($sortDirection) != 'asc' && strtolower($sortDirection) != 'desc') {
141  $response->addError(ApplicationError::get('SORT_DIRECTION_UNKNOWN'));
142  return false;
143  }
144  }
145  }
146  // do default validation
147  return parent::validate();
148  }
149 
150  /**
151  * @see BatchController::getWorkPackage()
152  */
153  protected function getWorkPackage($number) {
154  if ($number == 0) {
155  return ['name' => $this->getMessage()->getText('Initialization'),
156  'size' => 1, 'oids' => [1], 'callback' => 'initExport'];
157  }
158  }
159 
160  /**
161  * @see BatchController::getDownloadFile()
162  */
163  protected function getDownloadFile() {
164  return $this->getRequestValue('downloadFile');
165  }
166 
167  /**
168  * Initialize the CSV export (object ids parameter will be ignored)
169  * @param $oids The object ids to process
170  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
171  */
172  protected function initExport($oids) {
173  $persistenceFacade = $this->getPersistenceFacade();
174  $message = $this->getMessage();
175 
176  // get document definition
177  $docFile = $this->getDownloadFile();
178 
179  // delete export file
180  if (file_exists($docFile)) {
181  unlink($docFile);
182  }
183 
184  // get the query
185  $queryTerm = urldecode($this->getRequestValue('query'));
186 
187  // add sort term
188  $sortArray = null;
189  $orderBy = $this->getRequestValue('sortFieldName');
190  if (strlen($orderBy) > 0) {
191  $sortArray = [$orderBy." ".$this->getRequestValue('sortDirection')];
192  }
193 
194  // determine the type
195  $type = $this->getRequestValue('className');
196  $sourceId = $this->getRequestValue('sourceId');
197  $relation = $this->getRequestValue('relation');
198  if ($sourceId && $relation) {
199  // type is determined by the other end of the relation
200  $sourceNode = $persistenceFacade->load(new ObjectId($type, $sourceId));
201  $relationDescription = $sourceNode->getMapper()->getRelation($relation);
202  $type = $relationDescription->getOtherType();
203  }
204 
205  // get object ids of all nodes to export
206  $query = new StringQuery($type);
207  $query->setRQLConditionString($queryTerm);
208 
209  // add relation query, if requested
210  if ($relation && $sourceNode) {
211  $existingQuery = $query->getConditionString();
212  $query->setConditionString((strlen($existingQuery) > 0 ? $existingQuery.' AND ' : '').NodeUtil::getRelationQueryCondition($sourceNode, $relation));
213  }
214 
215  $oids = $query->execute(false, $sortArray);
216 
217  // get csv columns
218  $names = [];
219  $mapper = $persistenceFacade->getMapper($type);
220  foreach($mapper->getAttributes() as $attribute) {
221  $names[] = $attribute->getName();
222  }
223 
224  // initialize export file
225  $fileHandle = fopen($docFile, "a");
226  fputcsv($fileHandle, $names);
227  fclose($fileHandle);
228 
229  // create work packages for nodes
230  $nodesPerCall = $this->getRequestValue('nodesPerCall');
231  $this->addWorkPackage(
232  $message->getText('Exporting %0%', [$type]),
233  $nodesPerCall, $oids, 'exportNodes');
234  }
235 
236  /**
237  * Serialize all Nodes with given object ids to CSV
238  * @param $oids The object ids to process
239  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
240  */
241  protected function exportNodes($oids) {
242  if (sizeof($oids) > 0) {
243  $persistenceFacade = $this->getPersistenceFacade();
244  $permissionManager = $this->getPermissionManager();
245 
246  // get document definition
247  $docFile = $this->getDownloadFile();
248  $type = $oids[0]->getType();
249 
250  $mapper = $persistenceFacade->getMapper($type);
251  $attributes = $mapper->getAttributes();
252 
253  $translateValues = StringUtil::getBoolean($this->getRequestValue('translateValues'));
254 
255  // process nodes
256  $fileHandle = fopen($docFile, "a");
257  foreach ($oids as $oid) {
258  if ($permissionManager->authorize($oid, '', PersistenceAction::READ)) {
259  $node = $persistenceFacade->load($oid);
260  $values = [];
261  foreach ($attributes as $attribute) {
262  $inputType = $attribute->getInputType();
263  $value = $node->getValue($attribute->getName());
264  // translate values if requested
265  if ($translateValues) {
266  $value = ValueListProvider::translateValue($value, $inputType);
267  }
268  $values[] = $value;
269  }
270  fputcsv($fileHandle, $values);
271  }
272  }
273  fclose($fileHandle);
274  }
275  }
276 
277  /**
278  * @see BatchController::cleanup()
279  */
280  protected function cleanup() {
281  $downloadDir = dirname($this->getRequestValue('downloadFile'));
282  FileUtil::emptyDir($downloadDir);
283  rmdir($downloadDir);
284  parent::cleanup();
285  }
286 }
287 ?>
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
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
initialize(Request $request, Response $response)
CSVExportController exports instances of one type into a CSV file.
hasValue($name)
Check for existence of a value.
initExport($oids)
Initialize the CSV export (object ids parameter will be ignored)
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
getAction()
Get the name of the action.
getPermissionManager()
Get the PermissionManager instance.
Definition: Controller.php:291
StringQuery executes queries from a string representation.
Definition: StringQuery.php:53
getMessage()
Get the Message instance.
Definition: Controller.php:315
ValueListProvider provides lists of key/values to be used with list input controls.
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.
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.
getRequest()
Get the Request instance.
Definition: Controller.php:251
FileUtil provides basic support for file functionality like HTTP file upload.
Definition: FileUtil.php:22
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.
Controller is the base class of all controllers.
Definition: Controller.php:49
getResponse()
Get the Response instance.
Definition: Controller.php:259
NodeUtil provides services for the Node class.
Definition: NodeUtil.php:28
PersistenceAction values are used to define actions on PersistentObject instances.
exportNodes($oids)
Serialize all Nodes with given object ids to CSV.
static translateValue($value, $inputType, $language=null, $itemDelim=", ")
Translate a value with use of it's assoziated input type e.g.