wCMF  3.6
 All Classes Namespaces Files Functions Variables Groups Pages
class.SaveController.php
Go to the documentation of this file.
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2014 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of any of the following licenses
7  * at your choice:
8  *
9  * - GNU Lesser General Public License (LGPL)
10  * http://www.gnu.org/licenses/lgpl.html
11  * - Eclipse Public License (EPL)
12  * http://www.eclipse.org/org/documents/epl-v10.php
13  *
14  * See the license.txt file distributed with this work for
15  * additional information.
16  *
17  * $Id: class.SaveController.php 1462 2014-02-04 23:52:27Z iherwig $
18  */
19 require_once(BASE."wcmf/lib/presentation/class.Controller.php");
20 require_once(BASE."wcmf/lib/persistence/class.PersistenceFacade.php");
21 require_once(BASE."wcmf/lib/persistence/class.LockManager.php");
22 require_once(BASE."wcmf/lib/model/class.Node.php");
23 require_once(BASE."wcmf/lib/model/class.NodeUtil.php");
24 require_once(BASE."wcmf/lib/util/class.InifileParser.php");
25 require_once(BASE."wcmf/lib/util/class.FileUtil.php");
26 require_once(BASE."wcmf/lib/util/class.URIUtil.php");
27 require_once(BASE."wcmf/lib/util/class.GraphicsUtil.php");
28 require_once(BASE."wcmf/lib/util/class.SessionData.php");
29 require_once(BASE."wcmf/lib/util/class.FormUtil.php");
30 
31 /**
32  * @class SaveController
33  * @ingroup Controller
34  * @brief SaveController is a controller that saves Node data.
35  *
36  * <b>Input actions:</b>
37  * - unspecified: Save the given Node values
38  *
39  * <b>Output actions:</b>
40  * - @em ok In any case
41  *
42  * @param[in] <oid> A list of nodes defining what to save. Each node should only contain those values, that should be changed
43  * This may be achived by creating the node using the node constructor (instead of using PersistenceFacade::create)
44  * and setting the values on it.
45  * @param[in] uploadDir The directory where uploaded files should be placed (see SaveController::getUploadDir()) (optional)
46  * @param[out] oid The oid of the last Node saved
47  *
48  * Errors concerning single input fields are added to the session (the keys are the input field names)
49  *
50  * @author ingo herwig <ingo@wemove.com>
51  */
53 {
54  var $_fileUtil = null;
55  var $_graphicsUtil = null;
56 
57  /**
58  * @see Controller::hasView()
59  */
60  function hasView()
61  {
62  return false;
63  }
64  /**
65  * Save Node data.
66  * @return Array of given context and action 'ok' in every case.
67  * @see Controller::executeKernel()
68  */
69  function executeKernel()
70  {
71  $persistenceFacade = &PersistenceFacade::getInstance();
72  $lockManager = &LockManager::getInstance();
73  $session = &SessionData::getInstance();
74  $nodeUtil = new NodeUtil();
75 
76  // get field name delimiter
77  $fieldDelimiter = FormUtil::getInputFieldDelimiter();
78 
79  // for saving existing nodes we need not know the correct relations between the nodes
80  // so we store the nodes to save in an assoziative array (with their oids as keys) and iterate over it when saving
81  $saveArray = array();
82  $needCommit = false;
83 
84  // start the persistence transaction
85  $persistenceFacade->startTransaction();
86 
87  // set values for every node that is referenced by a data entry
88  $saveEntry = array();
89  foreach($this->_request->getData() as $key => $value)
90  {
92  && is_a($value, 'PersistentObject'))
93  {
94  $saveNode = &$value;
95  $saveEntry['oid'] = $key;
96 
97  // if the current user has a lock on the object, release it
98  $lockManager->releaseLock($saveEntry['oid']);
99 
100  // check if the object belonging to saveEntry is locked and continue with next if so
101  $lock = $lockManager->getLock($saveEntry['oid']);
102  if ($lock != null)
103  {
104  $this->appendErrorMsg($lockManager->getLockMessage($lock, $saveEntry['oid']));
105  continue;
106  }
107 
108  // iterate over all values given in the node
109  foreach ($saveNode->getDataTypes() as $dataType)
110  {
111  $saveEntry['dataType'] = $dataType;
112  foreach ($saveNode->getValueNames($dataType) as $name)
113  {
114  $saveEntry['name'] = $name;
115  $saveEntry['value'] = $saveNode->getValue($name, $dataType);
116 
117  // save uploaded file/ process array values
118  $isFile = false;
119  $deleteFile = false;
120  if (is_array($saveEntry['value']))
121  {
122  // save file
123  $result = $this->saveUploadFile($saveEntry);
124  // upload failed (present an error message and save the rest)
125  if ($result === false) {
126  ; // $this->_response->setAction('ok'); return true;
127  }
128  if ($result === true)
129  {
130  // no upload
131  // connect array values to a comma separated string
132  if (sizeof($saveEntry['value']) > 0)
133  $saveEntry['value'] = join($saveEntry['value'], ",");;
134  }
135  else
136  {
137  // success with probably altered filename
138  $saveEntry['value'] = $result;
139  $isFile = true;
140  }
141 
142  // delete file if demanded
143  if ($this->_request->hasValue('delete'.$fieldDelimiter.$key))
144  {
145  $saveEntry['value'] = '';
146  $deleteFile = true;
147  }
148  }
149 
150  // save node data
151  if ($saveEntry['oid'] != '')
152  {
153  // see if we have modified the node before or if we have to initially load it
154  // load node
155  $curOID = $saveEntry['oid'];
156  if (!isset($saveArray[$curOID]))
157  {
158  if ($this->isLocalizedRequest())
159  {
160  // create an empty object, if this is a localization request in order to
161  // make sure that only translated values are stored
162  $curType = PersistenceFacade::getOIDParameter($curOID, 'type');
163  $curNode = &$persistenceFacade->create($curType, BUILDDEPTH_SINGLE);
164  $curNode->setOID($curOID);
165  }
166  else {
167  // load the existing object, if this is a save request in order to merge
168  // the new with the existing values
169  $curNode = &$persistenceFacade->load($curOID, BUILDDEPTH_SINGLE);
170  }
171  if ($curNode == null)
172  {
173  $this->appendErrorMsg(Message::get("A Node with object id %1% does not exist.", array($curOID)));
174  return true;
175  }
176  // add missing attributes
177  $nodeUtil->completeNode($curNode);
178  }
179  // take existing node
180  else {
181  $curNode = &$saveArray[$curOID];
182  }
183 
184  // set data in node (prevent overriding old image values, if no image is uploaded)
185  if (ini_get('magic_quotes_gpc')) {
186  $saveEntry['value'] = stripslashes($saveEntry['value']);
187  }
188  if (!$isFile || ($isFile && !$deleteFile && $saveEntry['value'] != '') || ($isFile && $deleteFile))
189  {
190  $properties = $curNode->getValueProperties($saveEntry['name'], $saveEntry['dataType']);
191 
192  // remember old value and state ...
193  $oldValue = $curNode->getValue($saveEntry['name'], $saveEntry['dataType']);
194  $oldState = $curNode->getState();
195 
196  // ... and set the new value
197  $newValue = $saveEntry['value'];
198  if ($oldValue != $newValue) {
199  // handle file urls
200  if (strpos($properties['input_type'], 'file') !== false && strlen($newValue) > 0)
201  {
202  $filename = $newValue;
203  // prepend upload dir, if not already prepended
204  $uploadDir = $this->getUploadDir($curOID, $saveEntry['name'], $saveEntry['dataType']);
205  if (strpos($filename, $uploadDir) !== 0) {
206  $filename = $uploadDir.$newValue;
207  }
208  // make url relative, if it is absolute
209  else if (strpos($filename, UriUtil::getProtocolStr()) === 0)
210  {
211  $refURL = UriUtil::getProtocolStr().$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
212  $filename = URIUtil::makeRelative($filename, $refURL);
213  }
214  $newValue = $filename;
215  }
216  // set the new value
217  $curNode->setValue($saveEntry['name'], $newValue, $saveEntry['dataType']);
218  }
219  // call custom before-save handler
220  if ($this->modify($curNode, $saveEntry['name'], $saveEntry['dataType'], $oldValue)) {
221  $needCommit = true;
222  }
223  // get the modified new value
224  $newValue = $curNode->getValue($saveEntry['name'], $saveEntry['dataType']);
225 
226  // evaluate new value
227  $errorMessage = '';
228 
229  // validate the new value
230  $validationMsg = $curNode->validateValue($name, $newValue, $type);
231  $validationFailed = strlen($validationMsg) > 0 ? true : false;
232  $fileOk = $isFile ? $this->checkFile($curOID, $saveEntry['name'], $saveEntry['dataType'], $newValue) : true;
233  if (!$validationFailed && $fileOk)
234  {
235  if ($this->confirmSave($curNode, $saveEntry['name'], $saveEntry['dataType'], $newValue))
236  {
237  // new value is already set, so we just need to commit the change
238  $needCommit = true;
239  }
240  else {
241  $curNode->setValue($saveEntry['name'], $oldValue, $saveEntry['dataType']);
242  }
243  }
244  else
245  {
246  $errorMessage = $validationMsg;
247  }
248 
249  // check if evaluation failed
250  if (strlen($errorMessage) > 0)
251  {
252  $this->appendErrorMsg($errorMessage);
253 
254  // add error to session
255  $session->addError($key, $errorMessage);
256 
257  // new value is already set, so need to restore the old value and state
258  $curNode->setValue($saveEntry['name'], $oldValue, $saveEntry['dataType']);
259  $curNode->setState($oldState);
260  }
261  }
262 
263  // add node to save array if it was initially loaded (preserving its state)
264  $oldState = $curNode->getState();
265  if (!isset($saveArray[$curNode->getOID()]))
266  {
267  $saveArray[$curNode->getOID()] = &$curNode;
268  $curNode->setState($oldState);
269  }
270  }
271  }
272  }
273  }
274  }
275 
276  $saveOIDs = array_keys($saveArray);
277 
278  // commit changes
279  if ($needCommit)
280  {
281  $localization = Localization::getInstance();
282  for($i=0; $i<sizeof($saveOIDs); $i++)
283  {
284  $curObj = &$saveArray[$saveOIDs[$i]];
285  if ($this->isLocalizedRequest())
286  {
287  // store a translation for localized data
288  $localization->saveTranslation($curObj, $this->_request->getValue('language'));
289  }
290  else
291  {
292  // store the real object data
293  $curObj->save();
294  }
295  }
296  }
297 
298  // call custom after-save handler
299  for($i=0; $i<sizeof($saveOIDs); $i++) {
300  $this->afterSave($saveArray[$saveOIDs[$i]]);
301  }
302 
303  // end the persistence transaction
304  $persistenceFacade->commitTransaction();
305 
306  // return the oid of the last inserted object
307  if (sizeof($saveOIDs) > 0) {
308  $this->_response->setValue('oid', $saveOIDs[sizeof($saveOIDs)-1]);
309  }
310  $this->_response->setAction('ok');
311  return true;
312  }
313  /**
314  * Save uploaded file. This method calls checkFile which will prevent upload if returning false.
315  * @param data An assoziative array with keys 'oid', 'name', 'dataType', 'value' where value holds an assoziative
316  * array with keys 'name', 'type', 'size', 'tmp_name', 'error' as contained in the php $_FILES array.
317  * @return True if no upload happened (because no file was given) / False on error / The final filename if the upload was successful
318  */
319  function saveUploadFile($data)
320  {
321  $mediaFile = $data['value'];
322  if ($mediaFile['name'] != '')
323  {
324  // upload request -> see if upload was succesfull
325  if ($mediaFile['tmp_name'] != 'none')
326  {
327  // create FileUtil instance if not done already
328  if ($this->_fileUtil == null) {
329  $this->_fileUtil = new FileUtil();
330  }
331  // determine if max file size is defined for upload forms
332  $parser = &InifileParser::getInstance();
333  if(($maxFileSize = $parser->getValue('maxFileSize', 'htmlform')) === false) {
334  $maxFileSize = -1;
335  }
336  // check if file was actually uploaded
337  if (!is_uploaded_file($mediaFile['tmp_name']))
338  {
339  $this->appendErrorMsg(Message::get("Possible file upload attack: filename %1%.", array($mediaFile['name'])));
340  if ($maxFileSize != -1) {
341  $this->appendErrorMsg(Message::get("A possible reason is that the file size is too big (maximum allowed: %1% bytes).", array($maxFileSize)));
342  }
343  return false;
344  }
345 
346  // get upload directory
347  $uploadDir = $this->getUploadDir($data['oid'], $data['name'], $data['dataType']);
348 
349  // check file validity
350  if (!$this->checkFile($data['oid'], $data['name'], $data['dataType'], $mediaFile['tmp_name'], $mediaFile['type'])) {
351  return false;
352  }
353  // get the name for the uploaded file
354  $uploadFilename = $uploadDir.$this->getUploadFilename($data['oid'], $data['name'], $data['dataType'], $mediaFile['name']);
355 
356  // get upload parameters
357  $override = $this->shouldOverride($data['oid'], $data['name'], $data['dataType'], $uploadFilename);
358 
359  // upload file (mimeTypes parameter is set to null, because the mime type is already checked by checkFile method)
360  $filename = $this->_fileUtil->uploadFile($mediaFile, $uploadFilename, null, $maxFileSize, $override);
361  if (!$filename)
362  {
363  $this->appendErrorMsg($this->_fileUtil->getErrorMsg());
364  return false;
365  }
366  else {
367  return $filename;
368  }
369  }
370  else
371  {
372  $this->appendErrorMsg(Message::get("Upload failed for %1%.", array($mediaFile['name'])));
373  return false;
374  }
375  }
376 
377  // return true if no upload happened
378  return true;
379  }
380  /**
381  * Check if the file is valid for a given object value.
382  * @note subclasses will override this to implement special application requirements.
383  * @param oid The oid of the object
384  * @param valueName The name of the value of the object identified by oid
385  * @param dataType The data type value
386  * @param filename The name of the file to upload (including path)
387  * @param mimeType The mime type of the file (if null it will not be checked) [default: null]
388  * @return True/False whether the file is ok or not.
389  * @note The default implementation checks if the files mime type is contained in the mime types provided
390  * by the getMimeTypes method and if the dimensions provided by the getImageConstraints method are met. How to
391  * disable the image dimension check is described in the documentation of the getImageConstraints method.
392  */
393  function checkFile($oid, $valueName, $dataType, $filename, $mimeType=null)
394  {
395  // check mime type
396  if ($mimeType != null)
397  {
398  $mimeTypes = $this->getMimeTypes($oid, $valueName, $dataType);
399  if ($mimeTypes != null && !in_array($mimeType, $mimeTypes))
400  {
401  $this->appendErrorMsg(Message::get("File '%1%' has wrong mime type: %2%. Allowed types: %3%.", array($filename, $mimeType, join(", ", $mimeTypes))));
402  return false;
403  }
404  }
405 
406  // get required image dimensions
407  $imgConstraints = $this->getImageConstraints($oid, $valueName, $dataType);
408  $imgWidth = $imgConstraints['width'];
409  $imgHeight = $imgConstraints['height'];
410 
411  if ($imgWidth === false && $imgHeight === false) {
412  return true;
413  }
414  // create GraphicsUtil instance if not done already
415  if ($this->_graphicsUtil == null) {
416  $this->_graphicsUtil = new GraphicsUtil();
417  }
418  // check dimensions of new image
419  if ($imgWidth !== false) {
420  $checkWidth = $this->_graphicsUtil->isValidImageWidth($filename, $imgWidth[0], $imgWidth[1]);
421  }
422  else {
423  $checkWidth = true;
424  }
425  if ($imgHeight !== false) {
426  $checkHeight = $this->_graphicsUtil->isValidImageHeight($filename, $imgHeight[0], $imgHeight[1]);
427  }
428  else {
429  $checkHeight = true;
430  }
431  if(!($checkWidth && $checkHeight))
432  {
433  $this->appendErrorMsg($this->_graphicsUtil->getErrorMsg());
434  return false;
435  }
436 
437  return true;
438  }
439  /**
440  * Determine possible mime types for an object value.
441  * @note subclasses will override this to implement special application requirements.
442  * @param oid The oid of the object
443  * @param valueName The name of the value of the object identified by oid
444  * @param dataType The data type value
445  * @return An array containing the possible mime types or null meaning 'don't care'.
446  * @note The default implementation will return null.
447  */
448  function getMimeTypes($oid, $valueName, $dataType)
449  {
450  return null;
451  }
452  /**
453  * Get the image constraints for an object value.
454  * @note subclasses will override this to implement special application requirements.
455  * @param oid The oid of the object
456  * @param valueName The name of the value of the object identified by oid
457  * @param dataType The data type value
458  * @return An assoziative array with keys 'width' and 'height', which hold false meaning 'don't care' or arrays where the
459  * first entry is a pixel value and the second is 0 or 1 indicating that the dimension may be smaller than (0)
460  * or must exactly be (1) the pixel value.
461  * @note The default implementation will look for type.valueName.width or imgWidth and type.valueName.height or imgHeight
462  * keys in the configuration file (section 'media').
463  */
464  function getImageConstraints($oid, $valueName, $dataType)
465  {
466  $type = null;
467  if (PersistenceFacade::isValidOID($oid)) {
468  $type = PersistenceFacade::getOIDParameter($oid, 'type');
469  }
470  $parser = &InifileParser::getInstance();
471 
472  // defaults
473  $constraints = array('width' => false, 'height' => false);
474 
475  // check if type.valueName.width is defined in the configuration
476  if ($type && ($width = $parser->getValue($type.'.'.$valueName.'.width', 'Media', false)) !== false) {
477  $constraints['width'] = $width;
478  }
479  // check if imgWidth is defined in the configuration
480  else if (($width = $parser->getValue('imgWidth', 'Media', false)) !== false) {
481  $constraints['width'] = $width;
482  }
483 
484  // check if type.valueName.height is defined in the configuration
485  if ($type && ($height = $parser->getValue($type.'.'.$valueName.'.height', 'Media', false)) !== false) {
486  $constraints['height'] = $height;
487  }
488  // check if imgHeight is defined in the configuration
489  else if (($width = $parser->getValue('imgHeight', 'Media', false)) !== false) {
490  $constraints['width'] = $width;
491  }
492 
493  return $constraints;
494  }
495  /**
496  * Get the name for the uploaded file.
497  * @note subclasses will override this to implement special application requirements.
498  * @param oid The oid of the object
499  * @param valueName The name of the value of the object identified by oid
500  * @param dataType The data type value
501  * @param filename The name of the file to upload (including path)
502  * @return The filename
503  * @note The default implementation replaces all non alphanumerical characters except for ., -, _
504  * with underscores and turns the name to lower case.
505  */
506  function getUploadFilename($oid, $valueName, $dataType, $filename)
507  {
508  $filename = preg_replace("/[^a-zA-Z0-9\-_\.\/]+/", "_", $filename);
509  return $filename;
510  }
511  /**
512  * Determine what to do if a file with the same name already exists.
513  * @note subclasses will override this to implement special application requirements.
514  * @param oid The oid of the object
515  * @param valueName The name of the value of the object identified by oid
516  * @param dataType The data type value
517  * @param filename The name of the file to upload (including path)
518  * @return True/False wether to override the file or to create a new unique filename
519  * @note The default implementation returns true.
520  */
521  function shouldOverride($oid, $valueName, $dataType, $filename)
522  {
523  return true;
524  }
525  /**
526  * Get the name of the directory to upload a file to and make shure that it exists.
527  * @note subclasses will override this to implement special application requirements.
528  * @param oid The oid of the object which will hold the association to the file
529  * @param valueName The name of the value which will hold the association to the file
530  * @param dataType The data type value
531  * @return The directory name
532  * @note The default implementation will first look for a parameter 'uploadDir'
533  * and then, if it is not given, for an 'uploadDir.'.type key in the configuration file
534  * (section 'media') and finally for an 'uploadDir' key at the same place.
535  */
536  function getUploadDir($oid, $valueName, $dataType)
537  {
538  if ($this->_request->hasValue('uploadDir')) {
539  $uploadDir = $this->_request->getValue('uploadDir').'/';
540  }
541  else
542  {
543  $parser = &InifileParser::getInstance();
544  if (PersistenceFacade::isValidOID($oid)) {
545  $type = PersistenceFacade::getOIDParameter($oid, 'type');
546  // check if uploadDir.type is defined in the configuration
547  if ($type && ($dir = $parser->getValue('uploadDir.'.$type, 'Media', false)) !== false) {
548  $uploadDir = $dir;
549  }
550  else {
551  if(($dir = $parser->getValue('uploadDir', 'media')) !== false) {
552  $uploadDir = $dir;
553  }
554  }
555  }
556  }
557 
558  if (substr($uploadDir,-1) != '/') {
559  $uploadDir .= '/';
560  }
561  // asure that the directory exists
562  if (!is_dir($uploadDir)) {
563  FileUtil::mkdirRec($uploadDir);
564  }
565  return $uploadDir;
566  }
567  /**
568  * Confirm save action on given Node value.
569  * @note subclasses will override this to implement special application requirements.
570  * @param node A reference to the Node to confirm.
571  * @param valueName The name of the value to save.
572  * @param valueType The data type of the value to save.
573  * @param newValue The new value to set.
574  * @return True/False whether the value should be changed [default: true]. In case of false
575  * the assigned error message will be displayed
576  */
577  function confirmSave(&$node, $valueName, $valueType, $newValue)
578  {
579  return true;
580  }
581  /**
582  * Modify a given Node value before save action. The new value is already set.
583  * @note subclasses will override this to implement special application requirements.
584  * @param node A reference to the Node to modify.
585  * @param valueName The name of the value to save.
586  * @param valueType The data type of the value to save.
587  * @param oldValue The old value.
588  * @return True/False whether the Node was modified [default: false].
589  */
590  function modify(&$node, $valueName, $valueType, $oldValue)
591  {
592  return false;
593  }
594  /**
595  * Called after save.
596  * @note subclasses will override this to implement special application requirements.
597  * @param node A reference to the Node saved.
598  * @note The method is called for all save candidates even if they are not saved (use PersistentObject::getState() to confirm).
599  */
600  function afterSave(&$node) {}
601 }
602 ?>
GraphicsUtil provides support for graphic manipulation.
SaveController is a controller that saves Node data.
get($message, $parameters=null, $domain='', $lang='')
getMimeTypes($oid, $valueName, $dataType)
mkdirRec($dirname)
NodeUtil provides services for the Node class. All methods are static.
makeRelative($abs_uri, $base)
shouldOverride($oid, $valueName, $dataType, $filename)
Controller is the base class of all controllers. If a Controller has a view it is expected to reside ...
getUploadDir($oid, $valueName, $dataType)
checkFile($oid, $valueName, $dataType, $filename, $mimeType=null)
getOIDParameter($oid, $param, $validate=true)
getUploadFilename($oid, $valueName, $dataType, $filename)
getImageConstraints($oid, $valueName, $dataType)
static getInputFieldDelimiter()
confirmSave(&$node, $valueName, $valueType, $newValue)
FileUtil provides basic support for file functionality like HTTP file upload.
const BUILDDEPTH_SINGLE
modify(&$node, $valueName, $valueType, $oldValue)