0b7e709cd31aab5f128b031e6e3bb646859bcf85
[tine20] / tine20 / Tinebase / Relations.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Relations
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2008-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  * 
11  * @todo        re-enable the caching (but check proper invalidation first) -> see task #232
12  */
13
14 /**
15  * Class for handling relations between application records.
16  * @todo move json api specific stuff into the model
17  * 
18  * @package     Tinebase
19  * @subpackage  Relations 
20  */
21 class Tinebase_Relations
22 {
23     /**
24      * @var Tinebase_Relation_Backend_Sql
25      */
26     protected $_backend;
27     /**
28      * holds the instance of the singleton
29      *
30      * @var Tinebase_Relations
31      */
32     private static $instance = NULL;
33     
34     /**
35      * the constructor
36      *
37      */
38     private function __construct()
39     {
40         $this->_backend = new Tinebase_Relation_Backend_Sql();
41     }
42     
43     /**
44      * the singleton pattern
45      *
46      * @return Tinebase_Relations
47      */
48     public static function getInstance() 
49     {
50         if (self::$instance === NULL) {
51             self::$instance = new Tinebase_Relations();
52         }
53         return self::$instance;
54     }
55     
56     /**
57      * set all relations of a given record
58      * 
59      * NOTE: given relation data is expected to be an array atm.
60      * @todo check read ACL for new relations to existing records.
61      * 
62      * @param  string $_model           own model to get relations for
63      * @param  string $_backend         own backend to get relations for
64      * @param  string $_id              own id to get relations for 
65      * @param  array  $_relationData    data for relations to create
66      * @param  bool   $_ignoreACL       create relations without checking permissions
67      * @param  bool   $_inspectRelated  do update/create related records on the fly
68      * @param  bool   $_doCreateUpdateCheck do duplicate/freebusy/... checking for relations
69      * @return void
70      */
71     public function setRelations($_model,
72                                  $_backend,
73                                  $_id,
74                                  $_relationData,
75                                  $_ignoreACL = false,
76                                  $_inspectRelated = false,
77                                  $_doCreateUpdateCheck = false)
78     {
79         $relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
80         foreach((array) $_relationData as $relationData) {
81             if ($relationData instanceof Tinebase_Model_Relation) {
82                 $relations->addRecord($relationData);
83             } else {
84                 $relation = new Tinebase_Model_Relation(NULL, TRUE);
85                 $relation->setFromJsonInUsersTimezone($relationData);
86                 $relations->addRecord($relation);
87             }
88         }
89         
90         // own id sanitising
91         $relations->own_model   = $_model;
92         $relations->own_backend = $_backend;
93         $relations->own_id      = $_id;
94         
95         // convert related_record to record objects
96         // @todo move this to a relation json class / or to model->setFromJson
97         $this->_relatedRecordToObject($relations);
98         
99         // compute relations to add/delete
100         $currentRelations = $this->getRelations($_model, $_backend, $_id, NULL, array(), $_ignoreACL);
101         $currentIds   = $currentRelations->getArrayOfIds();
102         $relationsIds = $relations->getArrayOfIds();
103         
104         $toAdd = $relations->getIdLessIndexes();
105         $toDel = array_diff($currentIds, $relationsIds);
106         $toUpdate = array_intersect($currentIds, $relationsIds);
107
108         // prevent two empty related_id s of the same relation type
109         $emptyRelatedId = array();
110         foreach ($toAdd as $idx) {
111             if (empty($relations[$idx]->related_id)) {
112                 $relations[$idx]->related_id = Tinebase_Record_Abstract::generateUID();
113                 $emptyRelatedId[$idx] = true;
114             }
115         }
116         $this->_validateConstraintsConfig($_model, $relations, $toDel, $toUpdate);
117         
118         // break relations
119         foreach ($toDel as $relationId) {
120             $this->_backend->breakRelation($relationId);
121         }
122         
123         // add new relations
124         foreach ($toAdd as $idx) {
125             if(isset($emptyRelatedId[$idx])) {
126                 $relations[$idx]->related_id = null;
127                 $this->_setAppRecord($relations[$idx], $_doCreateUpdateCheck);
128             }
129             $this->_addRelation($relations[$idx]);
130         }
131         
132         // update relations
133         foreach ($toUpdate as $relationId) {
134             $current = $currentRelations[$currentRelations->getIndexById($relationId)];
135             $update = $relations[$relations->getIndexById($relationId)];
136             
137             // update related records if explicitly needed
138             if ($_inspectRelated) {
139                 // @todo do we need to omit so many fields?
140                 if (! $current->related_record->isEqual(
141                     $update->related_record, 
142                     array(
143                         'jpegphoto', 
144                         'creation_time', 
145                         'last_modified_time',
146                         'created_by',
147                         'last_modified_by',
148                         'is_deleted',
149                         'deleted_by',
150                         'deleted_time',
151                         'tags',
152                         'notes',
153                     )
154                 )) {
155                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
156                         . ' Related record diff: ' . print_r($current->related_record->diff($update->related_record)->toArray(), true));
157
158                     if ( !$update->related_record->has('container_id') ||
159                         Tinebase_Container::getInstance()->hasGrant(Tinebase_Core::getUser()->getId(), $update->related_record->container_id,
160                             array(Tinebase_Model_Grants::GRANT_EDIT, Tinebase_Model_Grants::GRANT_ADMIN)) ) {
161                         $this->_setAppRecord($update, $_doCreateUpdateCheck);
162                     } else {
163                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
164                             ' Permission denied to update related record');
165                     }
166                 }
167             }
168             
169             if (! $current->isEqual($update, array('related_record'))) {
170                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
171                     . ' Relation diff: ' . print_r($current->diff($update)->toArray(), true));
172                 
173                 $this->_updateRelation($update);
174             }
175         }
176     }
177     
178     /**
179      * returns the constraints config for the given models and their mirrored values (seen from the other side
180      * 
181      * @param array $models
182      * @return array
183      */
184     public static function getConstraintsConfigs($models)
185     {
186         if (! is_array($models)) {
187             $models = array($models);
188         }
189         $allApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
190         $ret = array();
191         
192         foreach ($models as $model) {
193         
194             $ownModel = explode('_Model_', $model);
195         
196             if (! class_exists($model) || ! in_array($ownModel[0], $allApplications)) {
197                 continue;
198             }
199             $cItems = $model::getRelatableConfig();
200             
201             $ownApplication = $ownModel[0];
202             $ownModel = $ownModel[1];
203         
204             if (is_array($cItems)) {
205                 foreach($cItems as $cItem) {
206         
207                     if (! array_key_exists('config', $cItem)) {
208                         continue;
209                     }
210         
211                     // own side
212                     $ownConfigItem = $cItem;
213                     $ownConfigItem['ownModel'] = $ownModel;
214                     $ownConfigItem['ownApp'] = $ownApplication;
215                     $ownConfigItem['ownRecordClassName'] = $ownApplication . '_Model_' . $ownModel;
216                     $ownConfigItem['relatedRecordClassName'] = $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'];
217                     
218                     $foreignConfigItem = array(
219                         'reverted'     => true,
220                         'ownApp'       => $cItem['relatedApp'],
221                         'ownModel'     => $cItem['relatedModel'],
222                         'relatedModel' => $ownModel,
223                         'relatedApp'   => $ownApplication,
224                         'default'      => array_key_exists('default', $cItem) ? $cItem['default'] : NULL,
225                         'ownRecordClassName' => $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'],
226                         'relatedRecordClassName' => $ownApplication . '_Model_' . $ownModel
227                     );
228         
229                     // KeyfieldConfigs
230                     if (array_key_exists('keyfieldConfig', $cItem)) {
231                         $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
232                         if ($cItem['keyfieldConfig']['from']){
233                             $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
234                         }
235                     }
236         
237                     $j=0;
238                     foreach ($cItem['config'] as $conf) {
239                         $max = explode(':',$conf['max']);
240                         $ownConfigItem['config'][$j]['max'] = intval($max[0]);
241         
242                         $foreignConfigItem['config'][$j] = $conf;
243                         $foreignConfigItem['config'][$j]['max'] = intval($max[1]);
244                         if ($conf['degree'] == 'sibling') {
245                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
246                         } else {
247                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
248                         }
249                         $j++;
250                     }
251                     
252                     $ret[] = $ownConfigItem;
253                     $ret[] = $foreignConfigItem;
254                 }
255             }
256         }
257         
258         return $ret;
259     }
260     
261     /**
262      * validate constraints from the own and the other side.
263      * this may be very expensive, if there are many constraints to check.
264      * 
265      * @param string $ownModel
266      * @param Tinebase_Record_RecordSet $relations
267      * @throws Tinebase_Exception_InvalidRelationConstraints
268      */
269     protected function _validateConstraintsConfig($ownModel, $relations, $toDelete = array(), $toUpdate = array())
270     {
271         if (! $relations->count()) {
272             return;
273         }
274         $relatedModels = array_unique($relations->related_model);
275         $relatedIds    = array_unique($relations->related_id);
276         
277         $toDelete      = is_array($toDelete) ? $toDelete : array();
278         $toUpdate      = is_array($toUpdate) ? $toUpdate : array();
279         $excludeCount  = array_merge($toDelete, $toUpdate);
280
281         $ownId         = $relations->getFirstRecord()->own_id;
282
283         // find out all models having a constraints config
284         $allModels = $relatedModels;
285         $allModels[] = $ownModel;
286         $allModels = array_unique($allModels);
287
288         $constraintsConfigs = self::getConstraintsConfigs($allModels);
289         $relatedConstraints = $this->_backend->countRelatedConstraints($ownModel, $relations, $excludeCount);
290         
291         $groups = array();
292         foreach($relations as $relation) {
293             $groups[] = $relation->related_model . '--' . $relation->type . '--' . $relation->own_id;
294         }
295         
296         $myConstraints = array_count_values($groups);
297
298         $groups = array();
299         foreach($relations as $relation) {
300             if (! in_array($relation->getId(), $excludeCount)) {
301                 $groups[] = $relation->own_model . '--' . $relation->type . '--' . $relation->related_id;
302             }
303         }
304         
305         foreach($relatedConstraints as $relC) {
306             for ($i = 0; $i < $relC['count']; $i++) {
307                 $groups[] = $relC['id'];
308             }
309         }
310         
311         $allConstraints = array_count_values($groups);
312
313         foreach ($constraintsConfigs as $cc) {
314             if (! isset($cc['config'])) {
315                 continue;
316             }
317             foreach($cc['config'] as $config) {
318                 
319                 $group = $cc['relatedRecordClassName'] . '--' . $config['type'];
320                 $idGroup = $group . '--' . $ownId;
321
322                 if (isset($myConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $myConstraints[$idGroup])) {
323                 
324                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
325                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the own side! ' . print_r($cc, 1));
326                     }
327                     throw new Tinebase_Exception_InvalidRelationConstraints();
328                 }
329                 
330                 // TODO: if the other side gets the config reverted here, validating constrains failes here on multiple update 
331                 foreach($relatedIds as $relatedId) {
332                     $idGroup = $group . '--' . $relatedId;
333                     
334                     if (isset($allConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $allConstraints[$idGroup])) {
335                         
336                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
337                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the other side! ' . print_r($cc, 1));
338                         }
339
340                         throw new Tinebase_Exception_InvalidRelationConstraints();
341                     }
342                 }
343             }
344         }
345     }
346     
347     /**
348      * get all relations of a given record
349      * - cache result if caching is activated
350      * 
351      * @param  string       $_model         own model to get relations for
352      * @param  string       $_backend       own backend to get relations for
353      * @param  string|array $_id            own id to get relations for
354      * @param  string       $_degree        only return relations of given degree
355      * @param  array        $_type          only return relations of given type
356      * @param  bool         $_ignoreACL     get relations without checking permissions
357      * @param  array        $_relatedModels only return relations having this related models
358      * @return Tinebase_Record_RecordSet of Tinebase_Model_Relation
359      */
360     public function getRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
361     {
362         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "  model: '$_model' backend: '$_backend' " 
363             // . 'ids: ' . print_r((array)$_id, true)
364         );
365         
366         $result = $this->_backend->getAllRelations($_model, $_backend, $_id, $_degree, $_type, FALSE, $_relatedModels);
367         $this->resolveAppRecords($result, $_ignoreACL);
368         
369         return $result;
370     }
371     
372     /**
373      * get all relations of all given records
374      * 
375      * @param  string $_model         own model to get relations for
376      * @param  string $_backend       own backend to get relations for
377      * @param  array  $_ids           own ids to get relations for
378      * @param  string $_degree        only return relations of given degree
379      * @param  array  $_type          only return relations of given type
380      * @param  bool   $_ignoreACL     get relations without checking permissions
381      * @param  array  $_relatedModels only return relations having this related model
382      * @return array  key from $_ids => Tinebase_Record_RecordSet of Tinebase_Model_Relation
383      */
384     public function getMultipleRelations($_model, $_backend, $_ids, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
385     {
386         // prepare a record set for each given id
387         $result = array();
388         foreach ($_ids as $key => $id) {
389             $result[$key] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array(),  true);
390         }
391         
392         // fetch all relations in a single set
393         $relations = $this->getRelations($_model, $_backend, $_ids, $_degree, $_type, $_ignoreACL, $_relatedModels);
394         
395         // sort relations into corrensponding sets
396         foreach ($relations as $relation) {
397             $keys = array_keys($_ids, $relation->own_id);
398             foreach ($keys as $key) {
399                 $result[$key]->addRecord($relation);
400             }
401         }
402         
403         return $result;
404     }
405     
406     /**
407      * converts related_records into their appropriate record objects
408      * @todo move to model->setFromJson
409      * 
410      * @param  Tinebase_Model_Relation|Tinebase_Record_RecordSet
411      * @return void
412      */
413     protected function _relatedRecordToObject($_relations)
414     {
415         if(! $_relations instanceof Tinebase_Record_RecordSet) {
416             $_relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array($_relations));
417         }
418         
419         foreach ($_relations as $relation) {
420             if (! is_string($relation->related_model)) {
421                 throw new Tinebase_Exception_InvalidArgument('missing relation model');
422             }
423
424             if (empty($relation->related_record) || $relation->related_record instanceof $relation->related_model) {
425                 continue;
426             }
427             
428             $data = Zend_Json::encode($relation->related_record);
429             $relation->related_record = new $relation->related_model();
430             $relation->related_record->setFromJsonInUsersTimezone($data);
431         }
432     }
433     
434     /**
435      * creates/updates application records
436      * 
437      * @param   Tinebase_Record_RecordSet of Tinebase_Model_Relation
438      * @param   bool $_doCreateUpdateCheck
439      * @throws  Tinebase_Exception_UnexpectedValue
440      */
441     protected function _setAppRecord($_relation, $_doCreateUpdateCheck = false)
442     {
443         if (! $_relation->related_record instanceof Tinebase_Record_Abstract) {
444             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
445                 . ' Relation: ' . print_r($_relation->toArray(), TRUE));
446             throw new Tinebase_Exception_UnexpectedValue('Related record is missing from relation.');
447         }
448         
449         $appController = Tinebase_Core::getApplicationInstance($_relation->related_model);
450         
451         if (! $_relation->related_record->getId()) {
452             $method = 'create';
453         } else {
454             $method = 'update';
455         }
456
457         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
458             . ' ' . ucfirst($method) . ' ' . $_relation->related_model . ' record.');
459         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
460             . ' Relation: ' . print_r($_relation->toArray(), TRUE));
461
462         $record = $appController->$method($_relation->related_record, $_doCreateUpdateCheck && $this->_doCreateUpdateCheck($_relation));
463         $_relation->related_id = $record->getId();
464         
465         switch ($_relation->related_model) {
466             case 'Addressbook_Model_Contact':
467                 $_relation->related_backend = ucfirst(Addressbook_Backend_Factory::SQL);
468                 break;
469             case 'Tasks_Model_Task':
470                 $_relation->related_backend = Tasks_Backend_Factory::SQL;
471                 break;
472             default:
473                 $_relation->related_backend = Tinebase_Model_Relation::DEFAULT_RECORD_BACKEND;
474                 break;
475         }
476     }
477
478     /**
479      * get configuration for duplicate/freebusy checks from relatable config
480      *
481      * @param $relation
482      *
483      * TODO relatable config should be an object with functions to get the needed information...
484      */
485     protected function _doCreateUpdateCheck($relation)
486     {
487         $relatableConfig = call_user_func($relation->own_model . '::getRelatableConfig');
488         foreach ($relatableConfig as $config) {
489             if ($relation->related_model === $config['relatedApp'] . '_Model_' . $config['relatedModel']
490                 && isset($config['createUpdateCheck'])
491             ) {
492                 return $config['createUpdateCheck'];
493             }
494         }
495         return false;
496     }
497     
498     /**
499      * resolved app records and fills the related_record property with the corresponding record
500      * 
501      * NOTE: With this, READ ACL is implicitly checked as non readable records won't get retuned!
502      * 
503      * @param  Tinebase_Record_RecordSet $_relations of Tinebase_Model_Relation
504      * @param  boolean $_ignoreACL 
505      * @return void
506      * 
507      * @todo    make getApplicationInstance work for tinebase record (Tinebase_Model_User for example)
508      */
509     protected function resolveAppRecords($_relations, $_ignoreACL = FALSE)
510     {
511         // separate relations by model
512         $modelMap = array();
513         foreach ($_relations as $relation) {
514             if (!(isset($modelMap[$relation->related_model]) || array_key_exists($relation->related_model, $modelMap))) {
515                 $modelMap[$relation->related_model] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
516             }
517             $modelMap[$relation->related_model]->addRecord($relation);
518         }
519         
520         // fill related_record
521         foreach ($modelMap as $modelName => $relations) {
522             
523             // check right
524             $split = explode('_Model_', $modelName);
525             $rightClass = $split[0] . '_Acl_Rights';
526             $rightName = 'manage_' . strtolower($split[1]) . 's';
527             
528             if (class_exists($rightClass)) {
529                 
530                 $ref = new ReflectionClass($rightClass);
531                 $u = Tinebase_Core::getUser();
532                 
533                 // if a manage right is defined and the user has no manage_record or admin right, remove relations having this record class as related model
534                 if (is_object($u) && $ref->hasConstant(strtoupper($rightName)) && (! $u->hasRight($split[0], $rightName)) && (! $u->hasRight($split[0], Tinebase_Acl_Rights::ADMIN))) {
535                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
536                         $_relations->removeRecords($relations);
537                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Skipping relation due to no manage right: ' . $modelName);
538                     }
539                     continue;
540                 }
541             }
542             
543             $getMultipleMethod = 'getMultiple';
544             
545             if ($modelName === 'Tinebase_Model_User') {
546                 // @todo add related backend here
547                 //$appController = Tinebase_User::factory($relations->related_backend);
548
549                 $appController = Tinebase_User::factory(Tinebase_User::getConfiguredBackend());
550                 $records = $appController->$getMultipleMethod($relations->related_id);
551             } else {
552                 try {
553                     $appController = Tinebase_Core::getApplicationInstance($modelName);
554                     if (method_exists($appController, $getMultipleMethod)) {
555                         $records = $appController->$getMultipleMethod($relations->related_id, $_ignoreACL);
556                         
557                         // resolve record alarms
558                         if (count($records) > 0 && $records->getFirstRecord()->has('alarms')) {
559                             $appController->getAlarms($records);
560                         }
561                     } else {
562                         throw new Tinebase_Exception_AccessDenied('Controller ' . get_class($appController) . ' has no method ' . $getMultipleMethod);
563                     }
564                 } catch (Tinebase_Exception_AccessDenied $tea) {
565                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
566                         . ' Removing relations from result. Got exception: ' . $tea->getMessage());
567                     $_relations->removeRecords($relations);
568                     continue;
569                 }
570             }
571
572             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
573                 " Resolving " . count($relations) . " relations");
574
575             foreach ($relations as $relation) {
576                 $recordIndex    = $records->getIndexById($relation->related_id);
577                 $relationIndex  = $_relations->getIndexById($relation->getId());
578                 if ($recordIndex !== false) {
579                     $_relations[$relationIndex]->related_record = $records[$recordIndex];
580                 } else {
581                     // delete relation from set, as READ ACL is obviously not granted 
582                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
583                         " removing $relation->related_model $relation->related_backend $relation->related_id (ACL)");
584                     unset($_relations[$relationIndex]);
585                 }
586             }
587         }
588     }
589     
590     /**
591      * get list of relations
592      *
593      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
594      * @param Tinebase_Model_Pagination|optional $_pagination
595      * @param boolean $_onlyIds
596      * @return Tinebase_Record_RecordSet|array
597      */
598     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_onlyIds = FALSE)
599     {
600         return $this->_backend->search($_filter, $_pagination, $_onlyIds);
601     }
602     
603     /**
604      * adds a new relation
605      * 
606      * @param   Tinebase_Model_Relation $_relation 
607      * @return  Tinebase_Model_Relation|NULL the new relation
608      * @throws  Tinebase_Exception_Record_Validation
609      */
610     protected function _addRelation(Tinebase_Model_Relation $_relation)
611     {
612         $_relation->created_by = Tinebase_Core::getUser()->getId();
613         $_relation->creation_time = Tinebase_DateTime::now();
614         if (!$_relation->isValid()) {
615             throw new Tinebase_Exception_Record_Validation('Relation is not valid' . print_r($_relation->getValidationErrors(),true));
616         }
617         
618         try {
619             $result = $this->_backend->addRelation($_relation);
620         } catch(Zend_Db_Statement_Exception $zse) {
621             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not add relation: ' . $zse->getMessage());
622             $result = NULL;
623         }
624         
625         return $result;
626     }
627     
628     /**
629      * update an existing relation
630      * 
631      * @param  Tinebase_Model_Relation $_relation 
632      * @return Tinebase_Model_Relation the updated relation
633      */
634     protected function _updateRelation($_relation)
635     {
636         $_relation->last_modified_by = Tinebase_Core::getUser()->getId();
637         $_relation->last_modified_time = Tinebase_DateTime::now();
638         
639         return $this->_backend->updateRelation($_relation);
640     }
641     
642     /**
643      * replaces all relations to or from a record with $sourceId to a record with $destinationId
644      * 
645      * @param string $sourceId
646      * @param string $destinationId
647      * @param string $model
648      * 
649      * @return array
650      */
651     public function transferRelations($sourceId, $destinationId, $model)
652     {
653         if (! Tinebase_Core::getUser()->hasRight('Tinebase', Tinebase_Acl_Rights::ADMIN)) {
654             throw new Tinebase_Exception_AccessDenied('Non admins of Tinebase aren\'t allowed to perform his operation!');
655         }
656         
657         return $this->_backend->transferRelations($sourceId, $destinationId, $model);
658     }
659
660     /**
661      * Deletes entries
662      *
663      * @param string|integer|Tinebase_Record_Interface|array $_id
664      * @return void
665      * @return int The number of affected rows.
666      */
667     public function delete($_id)
668     {
669         return $this->_backend->delete($_id);
670     }
671
672     /**
673      * remove all relations for application
674      *
675      * @param string $applicationName
676      *
677      * @return void
678      */
679     public function removeApplication($applicationName)
680     {
681         $this->_backend->removeApplication($applicationName);
682     }
683 }