SaveController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 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 
22 
23 /**
24  * SaveController is a controller that saves Node data.
25  *
26  * The controller supports the following actions:
27  *
28  * <div class="controller-action">
29  * <div> __Action__ _default_ </div>
30  * <div>
31  * Save the given Node values.
32  * | Parameter | Description
33  * |------------------------|-------------------------
34  * | _in_ / _out_ | Key/value pairs of serialized object ids and PersistentObject instances to save
35  * | _in_ `uploadDir` | The directory where attached files should be stored on the server (optional) (see SaveController::getUploadDir())
36  * | _out_ `oid` | The object id of the last newly created object
37  * | __Response Actions__ | |
38  * | `ok` | In all cases
39  * </div>
40  * </div>
41  *
42  * Errors concerning single input fields are added to the session (the keys are the input field names)
43  *
44  * @author ingo herwig <ingo@wemove.com>
45  */
46 class SaveController extends Controller {
47 
48  private $_fileUtil = null;
49 
50  /**
51  * Get the FileUtil instance
52  * @return FileUtil
53  */
54  protected function getFileUtil() {
55  if ($this->_fileUtil == null) {
56  $this->_fileUtil = new FileUtil();
57  }
58  return $this->_fileUtil;
59  }
60 
61  /**
62  * @see Controller::validate()
63  */
64  protected function validate() {
65  if (!$this->checkLanguageParameter()) {
66  return false;
67  }
68  // do default validation
69  return parent::validate();
70  }
71 
72  /**
73  * @see Controller::doExecute()
74  */
75  protected function doExecute() {
76  $persistenceFacade = $this->getPersistenceFacade();
77  $session = $this->getSession();
78  $request = $this->getRequest();
79  $response = $this->getResponse();
80  $message = $this->getMessage();
81 
82  // array of all involved nodes
83  $nodeArray = array();
84 
85  // array of oids to actually save
86  $saveOids = array();
87 
88  // array of oids to insert (subset of saveOids)
89  $insertOids = array();
90 
91  // start the persistence transaction
92  $transaction = $persistenceFacade->getTransaction();
93  $transaction->begin();
94  try {
95  // store all invalid parameters for later reference
96  $invalidOids = array();
97  $invalidAttributeNames = array();
98  $invalidAttributeValues = array();
99  $needCommit = false;
100  $curNode = null;
101 
102  // iterate over request values and check for oid/object pairs
103  $saveData = $request->getValues();
104  foreach ($saveData as $curOidStr => $curRequestObject) {
105  if ($curRequestObject instanceof PersistentObject && ($curOid = ObjectId::parse($curOidStr)) != null
106  && $curRequestObject->getOID() == $curOid) {
107 
108  // if the oid is a dummy, the object is supposed to be created instead of updated
109  $isNew = $curOid->containsDummyIds();
110 
111  // iterate over all values given in the node
112  $mapper = $curRequestObject->getMapper();
113  $pkValueNames = $mapper->getPkNames();
114  foreach ($curRequestObject->getValueNames() as $curValueName) {
115  // check if the attribute exists
116  if ($mapper && !$mapper->hasAttribute($curValueName) && !$mapper->hasRelation($curValueName)) {
117  $invalidAttributeNames[] = $curValueName;
118  }
119  // ignore primary key values, because they are immutable
120  if (!$isNew && in_array($curValueName, $pkValueNames)) {
121  continue;
122  }
123 
124  $curRequestValue = $curRequestObject->getValue($curValueName);
125 
126  // save uploaded file/ process array values
127  $isFile = false;
128  if (is_array($curRequestValue)) {
129  if ($this->isFileUpload($curRequestValue)) {
130  // save file
131  $filename = $this->saveUploadFile($curOid, $curValueName, $curRequestValue);
132  if ($filename != null) {
133  // success with probably altered filename
134  $curRequestValue = $filename;
135  }
136  $isFile = true;
137  }
138  else {
139  // no upload
140  // connect array values to a comma separated string if it's no a relation
141  if (!$mapper->hasRelation($curValueName)) {
142  $curRequestValue = join($curRequestValue, ",");
143  }
144  }
145  }
146 
147  // get the requested node
148  // see if we have already handled values of the node before or
149  // if we have to initially load/create it
150  if (!isset($nodeArray[$curOidStr])) {
151  // load/create the node initially
152  if ($this->isLocalizedRequest()) {
153  // create a detached object, if this is a localization request in order to
154  // save it manually later
155  $curNode = $persistenceFacade->create($curOid->getType(), BuildDepth::SINGLE);
156  // don't store changes on the original object
157  $transaction->detach($curNode->getOID());
158  $curNode->setOID($curOid);
159  $nodeArray[$curOidStr] = &$curNode;
160  }
161  else {
162  if ($isNew) {
163  // create a new object, if this is an insert request. set the object id
164  // of the request object for correct assignement in save arrays
165  $curNode = $persistenceFacade->create($curOid->getType(), BuildDepth::SINGLE);
166  }
167  else {
168  // load the existing object, if this is a save request in order to merge
169  // the new with the existing values
170  $curNode = $persistenceFacade->load($curOid, BuildDepth::SINGLE);
171  }
172  $nodeArray[$curOidStr] = &$curNode;
173  }
174  // the node could not be created from the oid
175  if ($curNode == null) {
176  $invalidOids[] = $curOidStr;
177  continue;
178  }
179  }
180  else {
181  // take the existing node
182  $curNode = &$nodeArray[$curOidStr];
183  }
184 
185  // set data in node (prevent overwriting old image values, if no image is uploaded)
186  if (!$isFile || ($isFile && sizeof($curRequestValue) > 0)) {
187  try {
188  // validate the new value
189  $curNode->validateValue($curValueName, $curRequestValue, $message);
190  if ($this->confirmSaveValue($curNode, $curValueName, $curRequestValue)) {
191  // set the new value
192  $oldValue = $curNode->getValue($curValueName);
193  $curNode->setValue($curValueName, $curRequestValue);
194  if ($oldValue != $curRequestValue) {
195  $needCommit = true;
196  }
197  }
198  }
199  catch(ValidationException $ex) {
200  $invalidAttributeValues[] = array('oid' => $curOidStr,
201  'parameter' => $curValueName, 'message' => $ex->getMessage());
202  // add error to session
203  $session->addError($curOidStr, $ex->getMessage());
204  }
205  }
206 
207  // add node to save array
208  if ($curNode->getState() != PersistentObject::STATE_CLEAN) {
209  // associative array to asure uniqueness
210  $saveOids[$curOidStr] = $curOidStr;
211  if ($isNew) {
212  $insertOids[$curOidStr] = $curOidStr;
213  }
214  }
215  }
216  }
217  }
218 
219  // add errors to the response
220  if (sizeof($invalidOids) > 0) {
221  $response->addError(ApplicationError::get('OID_INVALID',
222  array('invalidOids' => $invalidOids)));
223  }
224  if (sizeof($invalidAttributeNames) > 0) {
225  $response->addError(ApplicationError::get('ATTRIBUTE_NAME_INVALID',
226  array('invalidAttributeNames' => $invalidAttributeNames)));
227  }
228  if (sizeof($invalidAttributeValues) > 0) {
229  $response->addError(ApplicationError::get('ATTRIBUTE_VALUE_INVALID',
230  array('invalidAttributeValues' => $invalidAttributeValues)));
231  }
232 
233  // commit changes
234  if ($needCommit && !$response->hasErrors()) {
235  $localization = $this->getLocalization();
236  $saveOids = array_keys($saveOids);
237  for ($i=0, $count=sizeof($saveOids); $i<$count; $i++) {
238  $curOidStr = $saveOids[$i];
239  $curObject = &$nodeArray[$curOidStr];
240  $curOid = $curObject->getOid();
241 
242  // ask for confirmation
243  if ($this->confirmSave($curObject)) {
244  $this->beforeSave($curObject);
245  if ($this->isLocalizedRequest()) {
246  if (isset($insertOids[$curOidStr])) {
247  // translations are only allowed for existing objects
248  $response->addError(ApplicationError::get('PARAMETER_INVALID',
249  array('invalidParameters' => array('language'))));
250  }
251  else {
252  // store a translation for localized data
253  $localization->saveTranslation($curObject, $request->getValue('language'));
254  }
255  }
256  $this->afterSave($curObject);
257  }
258  else {
259  // detach object if not confirmed
260  $transaction->detach($curOid);
261  }
262  }
263  $transaction->commit();
264  }
265  else {
266  $transaction->rollback();
267  }
268  }
269  catch (PessimisticLockException $ex) {
270  $lock = $ex->getLock();
271  $response->addError(ApplicationError::get('OBJECT_IS_LOCKED',
272  array('lockedOids' => array($lock->getObjectId()->__toString()))));
273  $transaction->rollback();
274  }
275  catch (OptimisticLockException $ex) {
276  $currentState = $ex->getCurrentState();
277  $response->addError(ApplicationError::get('CONCURRENT_UPDATE',
278  array('currentState' => $currentState)));
279  $transaction->rollback();
280  }
281  catch (\Exception $ex) {
282  $this->getLogger()->error($ex);
283  $response->addError(ApplicationError::fromException($ex));
284  $transaction->rollback();
285  }
286 
287  // return the saved nodes
288  foreach ($nodeArray as $oidStr => $node) {
289  $response->setValue($node->getOid()->__toString(), $node);
290  }
291 
292  // return oid of the lastly created node
293  if (sizeof($insertOids) > 0) {
294  $keys = array_keys($insertOids);
295  $lastCreatedNode = $nodeArray[array_pop($keys)];
296  $lastCreatedOid = $lastCreatedNode->getOid();
297  $response->setValue('oid', $lastCreatedOid);
298  }
299 
300  $response->setAction('ok');
301  }
302 
303  /**
304  * Save uploaded file. This method calls checkFile which will prevent upload if returning false.
305  * @param $oid The ObjectId of the object to which the file is associated
306  * @param $valueName The name of the value to which the file is associated
307  * @param $data An assoziative array with keys 'name', 'type', 'tmp_name' as contained in the php $_FILES array.
308  * @return The final filename if the upload was successful, null on error
309  */
310  protected function saveUploadFile(ObjectId $oid, $valueName, array $data) {
311  if ($data['name'] != '') {
312  $response = $this->getResponse();
313  $message = $this->getMessage();
314  $fileUtil = $this->getFileUtil();
315 
316  // upload request -> see if upload was succesfull
317  if ($data['tmp_name'] == 'none') {
318  $response->addError(ApplicationError::get('GENERAL_ERROR',
319  array('message' => $message->getText("Upload failed for %0%.", array($data['name'])))));
320  return null;
321  }
322 
323  // check if file was actually uploaded
324  if (!is_uploaded_file($data['tmp_name'])) {
325  $message = $message->getText("Possible file upload attack: filename %0%.", array($data['name']));
326  $response->addError(ApplicationError::get('GENERAL_ERROR', array('message' => $message)));
327  return null;
328  }
329 
330  // get upload directory
331  $uploadDir = $this->getUploadDir($oid, $valueName);
332 
333  // get the name for the uploaded file
334  $uploadFilename = $uploadDir.$this->getUploadFilename($oid, $valueName, $data['name']);
335 
336  // check file validity
337  if (!$this->checkFile($oid, $valueName, $uploadFilename, $data['type'])) {
338  return null;
339  }
340 
341  // get upload parameters
342  $override = $this->shouldOverride($oid, $valueName, $uploadFilename);
343 
344  // upload file (mimeTypes parameter is set to null, because the mime type is already checked by checkFile method)
345  try {
346  return $fileUtil->uploadFile($data, $uploadFilename, null, $override);
347  } catch (\Exception $ex) {
348  $response->addError(ApplicationError::fromException($ex));
349  return null;
350  }
351  }
352  return null;
353  }
354 
355  /**
356  * Check if the given data defines a file upload. File uploads are defined in
357  * an assoziative array with keys 'name', 'type', 'tmp_name' as contained in the php $_FILES array.
358  * @param $data Array
359  * @return Boolean
360  */
361  protected function isFileUpload(array $data) {
362  return isset($data['name']) && isset($data['tmp_name']) && isset($data['type']);
363  }
364 
365  /**
366  * Check if the file is valid for a given object value. The implementation returns _true_.
367  * @note subclasses will override this to implement special application requirements.
368  * @param $oid The ObjectId of the object
369  * @param $valueName The name of the value of the object identified by oid
370  * @param $filename The name of the file to upload (including path)
371  * @param $mimeType The mime type of the file (if null it will not be checked) (default: _null_)
372  * @return Boolean whether the file is ok or not.
373  */
374  protected function checkFile(ObjectId $oid, $valueName, $filename, $mimeType=null) {
375  return true;
376  }
377 
378  /**
379  * Get the name for the uploaded file. The implementation replaces all non
380  * alphanumerical characters except for ., -, _ with underscores and turns the
381  * name to lower case.
382  * @note subclasses will override this to implement special application requirements.
383  * @param $oid The ObjectId of the object
384  * @param $valueName The name of the value of the object identified by oid
385  * @param $filename The name of the file to upload (including path)
386  * @return The filename
387  */
388  protected function getUploadFilename(ObjectId $oid, $valueName, $filename) {
389  $filename = preg_replace("/[^a-zA-Z0-9\-_\.\/]+/", "_", $filename);
390  return $filename;
391  }
392 
393  /**
394  * Determine what to do if a file with the same name already exists. The
395  * implementation returns _true_.
396  * @note subclasses will override this to implement special application requirements.
397  * @param $oid The ObjectId of the object
398  * @param $valueName The name of the value of the object identified by oid
399  * @param $filename The name of the file to upload (including path)
400  * @return Boolean whether to override the file or to create a new unique filename
401  */
402  protected function shouldOverride(ObjectId $oid, $valueName, $filename) {
403  return true;
404  }
405 
406  /**
407  * Get the name of the directory to upload a file to and make shure that it exists.
408  * The default implementation will first look for a parameter 'uploadDir'
409  * and then, if it is not given, for an 'uploadDir'. _type_ key in the configuration file
410  * (section 'media') and finally for an 'uploadDir' key at the same place.
411  * @note subclasses will override this to implement special application requirements.
412  * @param $oid The ObjectId of the object which will hold the association to the file
413  * @param $valueName The name of the value which will hold the association to the file
414  * @return The directory name
415  */
416  protected function getUploadDir(ObjectId $oid, $valueName) {
417  $request = $this->getRequest();
418  $fileUtil = $this->getFileUtil();
419  if ($request->hasValue('uploadDir')) {
420  $uploadDir = $fileUtil->realpath($request->getValue('uploadDir'));
421  }
422  else {
423  $config = $this->getConfiguration();
424  if (ObjectId::isValid($oid)) {
425  $persistenceFacade = $this->getPersistenceFacade();
426  $type = $persistenceFacade->getSimpleType($oid->getType());
427  // check if uploadDir.type is defined in the configuration
428  if ($type && ($dir = $config->getDirectoryValue('uploadDir.'.$type, 'media')) !== false) {
429  $uploadDir = $dir;
430  }
431  else {
432  if(($dir = $config->getDirectoryValue('uploadDir', 'media')) !== false) {
433  $uploadDir = $dir;
434  }
435  }
436  }
437  }
438  // asure that the directory exists
439  $fileUtil->mkdirRec($uploadDir);
440  return $uploadDir;
441  }
442 
443  /**
444  * Confirm save action on given Node value.
445  * @note subclasses will override this to implement special application requirements.
446  * @param $node A reference to the Node to confirm.
447  * @param $valueName The name of the value to save.
448  * @param $newValue The new value to set.
449  * @return Boolean whether the value should be changed (default: _true_).
450  */
451  protected function confirmSaveValue($node, $valueName, $newValue) {
452  return true;
453  }
454 
455  /**
456  * Confirm save action on given Node. This method is called before modify()
457  * @note subclasses will override this to implement special application requirements.
458  * @param $node A reference to the Node to confirm.
459  * @return Boolean whether the Node should be saved (default: _true_).
460  */
461  protected function confirmSave($node) {
462  return true;
463  }
464 
465  /**
466  * Called before save.
467  * @note subclasses will override this to implement special application requirements.
468  * @param $node A reference to the Node to be saved.
469  * @return Boolean whether the Node was modified (default: _false_).
470  */
471  protected function beforeSave($node) {
472  return false;
473  }
474 
475  /**
476  * Called after save.
477  * @note subclasses will override this to implement special application requirements.
478  * @param $node A reference to the Node saved.
479  */
480  protected function afterSave($node) {}
481 }
482 ?>
getRequest()
Get the Request instance.
Definition: Controller.php:190
getOID()
Get the object id of the PersistentObject.
saveUploadFile(ObjectId $oid, $valueName, array $data)
Save uploaded file.
getMessage()
Get the Message instance.
Definition: Controller.php:254
Controller is the base class of all controllers.
Definition: Controller.php:48
getFileUtil()
Get the FileUtil instance.
shouldOverride(ObjectId $oid, $valueName, $filename)
Determine what to do if a file with the same name already exists.
isLocalizedRequest()
Check if the current request is localized.
Definition: Controller.php:300
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
getLocalization()
Get the Localization instance.
Definition: Controller.php:246
OptimisticLockException signals an exception when trying to create an optimistic lock.
checkLanguageParameter()
Checks the language request parameter and adds an response error, if it is not contained in the Local...
Definition: Controller.php:315
ValidationException signals an exception in validation.
getUploadFilename(ObjectId $oid, $valueName, $filename)
Get the name for the uploaded file.
checkFile(ObjectId $oid, $valueName, $filename, $mimeType=null)
Check if the file is valid for a given object value.
getConfiguration()
Get the Configuration instance.
Definition: Controller.php:262
static fromException(\Exception $ex)
Factory method for transforming an exception into an ApplicationError instance.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:144
confirmSaveValue($node, $valueName, $newValue)
Confirm save action on given Node value.
Application controllers.
Definition: namespaces.php:3
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:132
static get($code, $data=null)
Factory method for retrieving a predefind error instance.
PessimisticLockException signals an exception when trying to create an pessimistic lock...
FileUtil provides basic support for file functionality like HTTP file upload.
Definition: FileUtil.php:22
getUploadDir(ObjectId $oid, $valueName)
Get the name of the directory to upload a file to and make shure that it exists.
getLogger()
Get the Logger instance.
Definition: Controller.php:206
getType()
Get the type (including namespace)
Definition: ObjectId.php:106
getResponse()
Get the Response instance.
Definition: Controller.php:198
getSession()
Get the Session instance.
Definition: Controller.php:214
isFileUpload(array $data)
Check if the given data defines a file upload.
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:222
confirmSave($node)
Confirm save action on given Node.
SaveController is a controller that saves Node data.
PersistentObject defines the interface of all persistent objects.