CSVImportController.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 
30 
31 /**
32  * CSVImportController imports instances of one type into the storage. It uses
33  * the `fgetcsv` function of PHP with the default values for delimiter, enclosing
34  * and escape character.
35  *
36  * The controller supports the following actions:
37  *
38  * <div class="controller-action">
39  * <div> __Action__ _default_ </div>
40  * <div>
41  * Initiate the import.
42  * | Parameter | Description
43  * |------------------------|-------------------------
44  * | _in_ `docFile` | The file upload as associative array with the following keys: 'name', 'type', 'tmp_name' (typically a $_FILES entry)
45  * | _in_ `className` | The entity type to import instances of
46  * | _in_ `nodesPerCall` | The number of nodes to process in one call (default: 50)
47  * </div>
48  * </div>
49  *
50  * For additional actions and parameters see [BatchController actions](@ref BatchController).
51  *
52  * @author ingo herwig <ingo@wemove.com>
53  */
55  const CACHE_KEY_STATS = 'stats';
56 
57  // default values, maybe overriden by corresponding request values (see above)
58  const NODES_PER_CALL = 50;
59 
60  private $cache = null;
61 
62  /**
63  * Constructor
64  * @param $session
65  * @param $persistenceFacade
66  * @param $permissionManager
67  * @param $actionMapper
68  * @param $localization
69  * @param $message
70  * @param $configuration
71  * @param $staticCache
72  */
73  public function __construct(Session $session,
74  PersistenceFacade $persistenceFacade,
75  PermissionManager $permissionManager,
76  ActionMapper $actionMapper,
77  Localization $localization,
78  Message $message,
79  Configuration $configuration,
80  Cache $staticCache) {
81  parent::__construct($session, $persistenceFacade, $permissionManager,
82  $actionMapper, $localization, $message, $configuration);
83  $this->cache = $staticCache;
84  $this->fileUtil = new FileUtil();
85  }
86 
87  /**
88  * @see Controller::initialize()
89  */
90  public function initialize(Request $request, Response $response) {
91  // initialize controller
92  if ($request->getAction() != 'continue') {
93  // set defaults (will be stored with first request)
94  if (!$request->hasValue('nodesPerCall')) {
95  $request->setValue('nodesPerCall', self::NODES_PER_CALL);
96  }
97 
98  // move the file upload
99  if ($request->hasValue('docFile')) {
100  $config = $this->getConfiguration();
101  $cacheBaseDir = WCMF_BASE.$config->getValue('cacheDir', 'StaticCache');
102  $cacheSection = 'csv-import-'.uniqid().'/cache';
103  $uploadDir = $cacheBaseDir.dirname($cacheSection).'/';
104  FileUtil::mkdirRec($uploadDir);
105  $uploadFile = $uploadDir.FileUtil::uploadFile($request->getValue('docFile'), $uploadDir.'data.csv');
106  $request->setValue('cacheSection', $cacheSection);
107  $request->setValue('uploadFile', $uploadFile);
108  }
109 
110  // get the csv header
111  if (($fileHandle = fopen($uploadFile, "r")) !== false &&
112  ($header = fgetcsv($fileHandle)) !== false) {
113  $request->setValue('csvHeader', $header);
114  }
115 
116  // initialize cache
117  $this->cache->put($cacheSection, self::CACHE_KEY_STATS, [
118  'processed' => 0,
119  'updated' => 0,
120  'created' => 0,
121  'skipped' => 0
122  ]);
123  }
124  // initialize parent controller after default request values are set
125  parent::initialize($request, $response);
126  }
127 
128  /**
129  * @see Controller::validate()
130  */
131  protected function validate() {
132  $request = $this->getRequest();
133  $response = $this->getResponse();
134  if ($request->getAction() != 'continue') {
135  if (!$request->hasValue('className') ||
136  !$this->getPersistenceFacade()->isKnownType($request->getValue('className'))) {
137  $response->addError(ApplicationError::get('PARAMETER_INVALID',
138  ['invalidParameters' => ['className']]));
139  return false;
140  }
141  // check if the upload succeeded and csv is valid
142  if (!$request->hasValue('csvHeader')) {
143  $response->addError(ApplicationError::get('PARAMETER_INVALID',
144  ['invalidParameters' => ['docFile']]));
145  return false;
146  }
147  // check for permission to create/update instances of className
148  if (!$this->getPermissionManager()->authorize($request->getValue('className'), '', PersistenceAction::CREATE) ||
149  !$this->getPermissionManager()->authorize($request->getValue('className'), '', PersistenceAction::UPDATE)) {
150  $response->addError(ApplicationError::get('PERMISSION_DENIED'));
151  return false;
152  }
153  }
154  // do default validation
155  return parent::validate();
156  }
157 
158  /**
159  * @see BatchController::getWorkPackage()
160  */
161  protected function getWorkPackage($number) {
162  if ($number == 0) {
163  return ['name' => $this->getMessage()->getText('Initialization'),
164  'size' => 1, 'oids' => [1], 'callback' => 'initImport'];
165  }
166  }
167 
168  /**
169  * Initialize the CSV import (object ids parameter will be ignored)
170  * @param $oids The object ids to process
171  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
172  */
173  protected function initImport($oids) {
174  $message = $this->getMessage();
175 
176  // get document definition
177  $type = $this->getRequestValue('className');
178  $docFile = $this->getRequestValue('uploadFile');
179  $cacheSection = $this->getRequestValue('cacheSection');
180 
181  // get stats from cache
182  $stats = $this->cache->get($cacheSection, self::CACHE_KEY_STATS);
183  $this->getResponse()->setValue('stats', $stats);
184 
185  // oids are line numbers
186  $importOids = [];
187  $file = new \SplFileObject($docFile);
188  $file->seek(1); // skip header
189  $eof = false;
190  while (!$eof) {
191  $content = $file->current();
192  if (strlen(trim($content)) > 0) {
193  $importOids[] = $file->key();
194  }
195  $file->next();
196  $eof = $file->eof();
197  }
198 
199  // create work packages for nodes
200  $nodesPerCall = $this->getRequestValue('nodesPerCall');
201  $this->addWorkPackage(
202  $message->getText('Importing %0%', [$type]),
203  $nodesPerCall, $importOids, 'importNodes');
204  }
205 
206  /**
207  * Serialize all Nodes with given object ids to CSV
208  * @param $oids The object ids to process
209  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
210  */
211  protected function importNodes($oids) {
212  $this->requireTransaction();
213  $persistenceFacade = $this->getPersistenceFacade();
214  $permissionManager = $this->getPermissionManager();
215 
216  // get document definition
217  $type = $this->getRequestValue('className');
218  $docFile = $this->getRequestValue('uploadFile');
219  $header = $this->getRequestValue('csvHeader');
220  $cacheSection = $this->getRequestValue('cacheSection');
221 
222  // get stats from cache
223  $stats = $this->cache->get($cacheSection, self::CACHE_KEY_STATS);
224 
225  // get type definition
226  $mapper = $persistenceFacade->getMapper($type);
227  $pkNames = $mapper->getPkNames();
228 
229  // process csv lines
230  $file = new \SplFileObject($docFile);
231  $file->setFlags(\SplFileObject::READ_CSV);
232  foreach ($oids as $oid) {
233  $file->seek($oid);
234  // read line
235  $data = array_combine($header, $file->current());
236 
237  // get primary key values
238  $pkValues = [];
239  foreach ($pkNames as $pkName) {
240  if (!empty($data[$pkName])) {
241  $pkValues[] = $data[$pkName];
242  }
243  }
244 
245  // get object to create/update
246  $isUpdate = sizeof($pkValues) == sizeof($pkNames);
247  if ($isUpdate) {
248  $oid = new ObjectId($type, $pkValues);
249  if ($permissionManager->authorize($oid, '', PersistenceAction::READ) &&
250  $permissionManager->authorize($oid, '', PersistenceAction::UPDATE)) {
251  $obj = $persistenceFacade->load($oid);
252  }
253  }
254  else {
255  if ($permissionManager->authorize($type, '', PersistenceAction::CREATE)) {
256  $obj = $persistenceFacade->create($type);
257  }
258  }
259 
260  // update the object
261  if ($obj != null) {
262  foreach ($data as $name => $value) {
263  if (!in_array($name, $pkNames) && $mapper->hasAttribute($name)) {
264  $value = empty($value) ? null : $value;
265  if ($value !== $obj->getValue($name)) {
266  $obj->setValue($name, $value);
267  }
268  }
269  }
270  $state = $obj->getState();
271  if ($state == PersistentObject::STATE_NEW) {
272  $stats['created']++;
273  }
274  elseif ($state == PersistentObject::STATE_DIRTY) {
275  $stats['updated']++;
276  }
277  else {
278  $stats['skipped']++;
279  }
280  }
281  else {
282  $stats['skipped']++;
283  }
284  $stats['processed']++;
285  }
286 
287  // update stats
288  $this->cache->put($cacheSection, self::CACHE_KEY_STATS, $stats);
289  $this->getResponse()->setValue('stats', $stats);
290  }
291 
292  /**
293  * @see BatchController::cleanup()
294  */
295  protected function cleanup() {
296  $uploadDir = dirname($this->getRequestValue('uploadFile'));
297  FileUtil::emptyDir($uploadDir);
298  rmdir($uploadDir);
299  parent::cleanup();
300  }
301 }
302 ?>
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
CSVImportController imports instances of one type into the storage.
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
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration, Cache $staticCache)
Constructor.
hasValue($name)
Check for existence of a value.
importNodes($oids)
Serialize all Nodes with given object ids to CSV.
initialize(Request $request, Response $response)
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
getAction()
Get the name of the action.
getPermissionManager()
Get the PermissionManager instance.
Definition: Controller.php:291
initImport($oids)
Initialize the CSV import (object ids parameter will be ignored)
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.
getMessage()
Get the Message instance.
Definition: Controller.php:315
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.
PersistenceFacade defines the interface for PersistenceFacade implementations.
getRequestValue($name)
Get a value from the initial request.
getValue($name, $default=null, $validateDesc=null, $suppressException=false)
Get a value.
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
PersistentObject defines the interface of all persistent objects.
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
getResponse()
Get the Response instance.
Definition: Controller.php:259
PermissionManager implementations are used to handle all authorization requests.
PersistenceAction values are used to define actions on PersistentObject instances.
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