0012024: remember popup window size in client state
[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 = $this->_getRelationIds($relations, $currentRelations);
103         
104         $toAdd = $relations->getIdLessIndexes();
105         $toDel = array_diff($currentIds, $relationsIds);
106         $toUpdate = array_intersect($currentIds, $relationsIds);
107
108         // prevent two empty related_ids 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      * appends missing relation ids if related records + type match
180      *
181      * @param $relations
182      * @param $currentRelations
183      * @return mixed
184      */
185     protected function _getRelationIds($relations, $currentRelations)
186     {
187         $clonedRelations = clone $relations;
188
189         if (count($currentRelations) > 0) {
190             foreach ($clonedRelations as $relation) {
191                 if ($relation->getId()) {
192                     continue;
193                 }
194
195                 // if relation has no id, maybe we have the same relation already in current relations
196                 $subset = $currentRelations->filter('own_id', $relation->own_id)
197                     ->filter('related_id', $relation->related_id)
198                     ->filter('type', $relation->type);
199
200                 if (count($subset) === 1) {
201                     // remove and add to make sure index is updated in record set
202                     $relations->removeRecord($relation);
203                     $relation->setId($subset->getFirstRecord()->getId());
204                     $relations->addRecord($relation);
205                     //$result[] = $subset->getFirstRecord()->getId();
206                 }
207             }
208         }
209
210         $result = $relations->getArrayOfIds();
211
212         return $result;
213     }
214
215     /**
216      * returns the constraints config for the given models and their mirrored values (seen from the other side
217      * 
218      * @param array $models
219      * @return array
220      */
221     public static function getConstraintsConfigs($models)
222     {
223         if (! is_array($models)) {
224             $models = array($models);
225         }
226         $allApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
227         $ret = array();
228         
229         foreach ($models as $model) {
230         
231             $ownModel = explode('_Model_', $model);
232         
233             if (! class_exists($model) || ! in_array($ownModel[0], $allApplications)) {
234                 continue;
235             }
236             $cItems = $model::getRelatableConfig();
237             
238             $ownApplication = $ownModel[0];
239             $ownModel = $ownModel[1];
240         
241             if (is_array($cItems)) {
242                 foreach($cItems as $cItem) {
243         
244                     if (! array_key_exists('config', $cItem)) {
245                         continue;
246                     }
247         
248                     // own side
249                     $ownConfigItem = $cItem;
250                     $ownConfigItem['ownModel'] = $ownModel;
251                     $ownConfigItem['ownApp'] = $ownApplication;
252                     $ownConfigItem['ownRecordClassName'] = $ownApplication . '_Model_' . $ownModel;
253                     $ownConfigItem['relatedRecordClassName'] = $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'];
254                     
255                     $foreignConfigItem = array(
256                         'reverted'     => true,
257                         'ownApp'       => $cItem['relatedApp'],
258                         'ownModel'     => $cItem['relatedModel'],
259                         'relatedModel' => $ownModel,
260                         'relatedApp'   => $ownApplication,
261                         'default'      => array_key_exists('default', $cItem) ? $cItem['default'] : NULL,
262                         'ownRecordClassName' => $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'],
263                         'relatedRecordClassName' => $ownApplication . '_Model_' . $ownModel
264                     );
265         
266                     // KeyfieldConfigs
267                     if (array_key_exists('keyfieldConfig', $cItem)) {
268                         $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
269                         if ($cItem['keyfieldConfig']['from']){
270                             $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
271                         }
272                     }
273         
274                     $j=0;
275                     foreach ($cItem['config'] as $conf) {
276                         $max = explode(':',$conf['max']);
277                         $ownConfigItem['config'][$j]['max'] = intval($max[0]);
278         
279                         $foreignConfigItem['config'][$j] = $conf;
280                         $foreignConfigItem['config'][$j]['max'] = intval($max[1]);
281                         if ($conf['degree'] == 'sibling') {
282                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
283                         } else {
284                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
285                         }
286                         $j++;
287                     }
288                     
289                     $ret[] = $ownConfigItem;
290                     $ret[] = $foreignConfigItem;
291                 }
292             }
293         }
294         
295         return $ret;
296     }
297     
298     /**
299      * validate constraints from the own and the other side.
300      * this may be very expensive, if there are many constraints to check.
301      * 
302      * @param string $ownModel
303      * @param Tinebase_Record_RecordSet $relations
304      * @throws Tinebase_Exception_InvalidRelationConstraints
305      */
306     protected function _validateConstraintsConfig($ownModel, $relations, $toDelete = array(), $toUpdate = array())
307     {
308         if (! $relations->count()) {
309             return;
310         }
311         $relatedModels = array_unique($relations->related_model);
312         $relatedIds    = array_unique($relations->related_id);
313         
314         $toDelete      = is_array($toDelete) ? $toDelete : array();
315         $toUpdate      = is_array($toUpdate) ? $toUpdate : array();
316         $excludeCount  = array_merge($toDelete, $toUpdate);
317
318         $ownId         = $relations->getFirstRecord()->own_id;
319
320         // find out all models having a constraints config
321         $allModels = $relatedModels;
322         $allModels[] = $ownModel;
323         $allModels = array_unique($allModels);
324
325         $constraintsConfigs = self::getConstraintsConfigs($allModels);
326         $relatedConstraints = $this->_backend->countRelatedConstraints($ownModel, $relations, $excludeCount);
327         
328         $groups = array();
329         foreach($relations as $relation) {
330             $groups[] = $relation->related_model . '--' . $relation->type . '--' . $relation->own_id;
331         }
332         
333         $myConstraints = array_count_values($groups);
334
335         $groups = array();
336         foreach($relations as $relation) {
337             if (! in_array($relation->getId(), $excludeCount)) {
338                 $groups[] = $relation->own_model . '--' . $relation->type . '--' . $relation->related_id;
339             }
340         }
341         
342         foreach($relatedConstraints as $relC) {
343             for ($i = 0; $i < $relC['count']; $i++) {
344                 $groups[] = $relC['id'];
345             }
346         }
347         
348         $allConstraints = array_count_values($groups);
349
350         foreach ($constraintsConfigs as $cc) {
351             if (! isset($cc['config'])) {
352                 continue;
353             }
354             foreach($cc['config'] as $config) {
355                 
356                 $group = $cc['relatedRecordClassName'] . '--' . $config['type'];
357                 $idGroup = $group . '--' . $ownId;
358
359                 if (isset($myConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $myConstraints[$idGroup])) {
360                 
361                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
362                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the own side! ' . print_r($cc, 1));
363                     }
364                     throw new Tinebase_Exception_InvalidRelationConstraints();
365                 }
366                 
367                 // TODO: if the other side gets the config reverted here, validating constrains failes here on multiple update 
368                 foreach($relatedIds as $relatedId) {
369                     $idGroup = $group . '--' . $relatedId;
370                     
371                     if (isset($allConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $allConstraints[$idGroup])) {
372                         
373                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
374                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the other side! ' . print_r($cc, 1));
375                         }
376
377                         throw new Tinebase_Exception_InvalidRelationConstraints();
378                     }
379                 }
380             }
381         }
382     }
383     
384     /**
385      * get all relations of a given record
386      * - cache result if caching is activated
387      * 
388      * @param  string       $_model         own model to get relations for
389      * @param  string       $_backend       own backend to get relations for
390      * @param  string|array $_id            own id to get relations for
391      * @param  string       $_degree        only return relations of given degree
392      * @param  array        $_type          only return relations of given type
393      * @param  bool         $_ignoreACL     get relations without checking permissions
394      * @param  array        $_relatedModels only return relations having this related models
395      * @return Tinebase_Record_RecordSet of Tinebase_Model_Relation
396      */
397     public function getRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
398     {
399         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "  model: '$_model' backend: '$_backend' " 
400             // . 'ids: ' . print_r((array)$_id, true)
401         );
402         
403         $result = $this->_backend->getAllRelations($_model, $_backend, $_id, $_degree, $_type, FALSE, $_relatedModels);
404         $this->resolveAppRecords($result, $_ignoreACL);
405         
406         return $result;
407     }
408     
409     /**
410      * get all relations of all given records
411      * 
412      * @param  string $_model         own model to get relations for
413      * @param  string $_backend       own backend to get relations for
414      * @param  array  $_ids           own ids to get relations for
415      * @param  string $_degree        only return relations of given degree
416      * @param  array  $_type          only return relations of given type
417      * @param  bool   $_ignoreACL     get relations without checking permissions
418      * @param  array  $_relatedModels only return relations having this related model
419      * @return array  key from $_ids => Tinebase_Record_RecordSet of Tinebase_Model_Relation
420      */
421     public function getMultipleRelations($_model, $_backend, $_ids, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
422     {
423         // prepare a record set for each given id
424         $result = array();
425         foreach ($_ids as $key => $id) {
426             $result[$key] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array(),  true);
427         }
428         
429         // fetch all relations in a single set
430         $relations = $this->getRelations($_model, $_backend, $_ids, $_degree, $_type, $_ignoreACL, $_relatedModels);
431         
432         // sort relations into corrensponding sets
433         foreach ($relations as $relation) {
434             $keys = array_keys($_ids, $relation->own_id);
435             foreach ($keys as $key) {
436                 $result[$key]->addRecord($relation);
437             }
438         }
439         
440         return $result;
441     }
442     
443     /**
444      * converts related_records into their appropriate record objects
445      * @todo move to model->setFromJson
446      * 
447      * @param  Tinebase_Model_Relation|Tinebase_Record_RecordSet
448      * @return void
449      */
450     protected function _relatedRecordToObject($_relations)
451     {
452         if(! $_relations instanceof Tinebase_Record_RecordSet) {
453             $_relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array($_relations));
454         }
455         
456         foreach ($_relations as $relation) {
457             if (! is_string($relation->related_model)) {
458                 throw new Tinebase_Exception_InvalidArgument('missing relation model');
459             }
460
461             if (empty($relation->related_record) || $relation->related_record instanceof $relation->related_model) {
462                 continue;
463             }
464             
465             $data = Zend_Json::encode($relation->related_record);
466             $relation->related_record = new $relation->related_model();
467             $relation->related_record->setFromJsonInUsersTimezone($data);
468         }
469     }
470     
471     /**
472      * creates/updates application records
473      * 
474      * @param   Tinebase_Record_RecordSet of Tinebase_Model_Relation
475      * @param   bool $_doCreateUpdateCheck
476      * @throws  Tinebase_Exception_UnexpectedValue
477      */
478     protected function _setAppRecord($_relation, $_doCreateUpdateCheck = false)
479     {
480         if (! $_relation->related_record instanceof Tinebase_Record_Abstract) {
481             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
482                 . ' Relation: ' . print_r($_relation->toArray(), TRUE));
483             throw new Tinebase_Exception_UnexpectedValue('Related record is missing from relation.');
484         }
485         
486         $appController = Tinebase_Core::getApplicationInstance($_relation->related_model);
487         
488         if (! $_relation->related_record->getId()) {
489             $method = 'create';
490         } else {
491             $method = 'update';
492         }
493
494         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
495             . ' ' . ucfirst($method) . ' ' . $_relation->related_model . ' record.');
496         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
497             . ' Relation: ' . print_r($_relation->toArray(), TRUE));
498
499         $record = $appController->$method($_relation->related_record, $_doCreateUpdateCheck && $this->_doCreateUpdateCheck($_relation));
500         $_relation->related_id = $record->getId();
501         
502         switch ($_relation->related_model) {
503             case 'Addressbook_Model_Contact':
504                 $_relation->related_backend = ucfirst(Addressbook_Backend_Factory::SQL);
505                 break;
506             case 'Tasks_Model_Task':
507                 $_relation->related_backend = Tasks_Backend_Factory::SQL;
508                 break;
509             default:
510                 $_relation->related_backend = Tinebase_Model_Relation::DEFAULT_RECORD_BACKEND;
511                 break;
512         }
513     }
514
515     /**
516      * get configuration for duplicate/freebusy checks from relatable config
517      *
518      * @param $relation
519      *
520      * TODO relatable config should be an object with functions to get the needed information...
521      */
522     protected function _doCreateUpdateCheck($relation)
523     {
524         $relatableConfig = call_user_func($relation->own_model . '::getRelatableConfig');
525         foreach ($relatableConfig as $config) {
526             if ($relation->related_model === $config['relatedApp'] . '_Model_' . $config['relatedModel']
527                 && isset($config['createUpdateCheck'])
528             ) {
529                 return $config['createUpdateCheck'];
530             }
531         }
532         return false;
533     }
534     
535     /**
536      * resolved app records and fills the related_record property with the corresponding record
537      * 
538      * NOTE: With this, READ ACL is implicitly checked as non readable records won't get retuned!
539      * 
540      * @param  Tinebase_Record_RecordSet $_relations of Tinebase_Model_Relation
541      * @param  boolean $_ignoreACL 
542      * @return void
543      * 
544      * @todo    make getApplicationInstance work for tinebase record (Tinebase_Model_User for example)
545      */
546     protected function resolveAppRecords($_relations, $_ignoreACL = FALSE)
547     {
548         // separate relations by model
549         $modelMap = array();
550         foreach ($_relations as $relation) {
551             if (!(isset($modelMap[$relation->related_model]) || array_key_exists($relation->related_model, $modelMap))) {
552                 $modelMap[$relation->related_model] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
553             }
554             $modelMap[$relation->related_model]->addRecord($relation);
555         }
556         
557         // fill related_record
558         foreach ($modelMap as $modelName => $relations) {
559             
560             // check right
561             $split = explode('_Model_', $modelName);
562             $rightClass = $split[0] . '_Acl_Rights';
563             $rightName = 'manage_' . strtolower($split[1]) . 's';
564             
565             if (class_exists($rightClass)) {
566                 
567                 $ref = new ReflectionClass($rightClass);
568                 $u = Tinebase_Core::getUser();
569                 
570                 // 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
571                 if (is_object($u) && $ref->hasConstant(strtoupper($rightName)) && (! $u->hasRight($split[0], $rightName)) && (! $u->hasRight($split[0], Tinebase_Acl_Rights::ADMIN))) {
572                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
573                         $_relations->removeRecords($relations);
574                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Skipping relation due to no manage right: ' . $modelName);
575                     }
576                     continue;
577                 }
578             }
579             
580             $getMultipleMethod = 'getMultiple';
581             
582             if ($modelName === 'Tinebase_Model_User') {
583                 // @todo add related backend here
584                 //$appController = Tinebase_User::factory($relations->related_backend);
585
586                 $appController = Tinebase_User::factory(Tinebase_User::getConfiguredBackend());
587                 $records = $appController->$getMultipleMethod($relations->related_id);
588             } else {
589                 try {
590                     $appController = Tinebase_Core::getApplicationInstance($modelName);
591                     if (method_exists($appController, $getMultipleMethod)) {
592                         $records = $appController->$getMultipleMethod($relations->related_id, $_ignoreACL);
593                         
594                         // resolve record alarms
595                         if (count($records) > 0 && $records->getFirstRecord()->has('alarms')) {
596                             $appController->getAlarms($records);
597                         }
598                     } else {
599                         throw new Tinebase_Exception_AccessDenied('Controller ' . get_class($appController) . ' has no method ' . $getMultipleMethod);
600                     }
601                 } catch (Tinebase_Exception_AccessDenied $tea) {
602                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
603                         . ' Removing relations from result. Got exception: ' . $tea->getMessage());
604                     $_relations->removeRecords($relations);
605                     continue;
606                 }
607             }
608
609             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
610                 " Resolving " . count($relations) . " relations");
611
612             foreach ($relations as $relation) {
613                 $recordIndex    = $records->getIndexById($relation->related_id);
614                 $relationIndex  = $_relations->getIndexById($relation->getId());
615                 if ($recordIndex !== false) {
616                     $_relations[$relationIndex]->related_record = $records[$recordIndex];
617                 } else {
618                     // delete relation from set, as READ ACL is obviously not granted 
619                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
620                         " removing $relation->related_model $relation->related_backend $relation->related_id (ACL)");
621                     unset($_relations[$relationIndex]);
622                 }
623             }
624         }
625     }
626     
627     /**
628      * get list of relations
629      *
630      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
631      * @param Tinebase_Model_Pagination|optional $_pagination
632      * @param boolean $_onlyIds
633      * @return Tinebase_Record_RecordSet|array
634      */
635     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_onlyIds = FALSE)
636     {
637         return $this->_backend->search($_filter, $_pagination, $_onlyIds);
638     }
639     
640     /**
641      * adds a new relation
642      * 
643      * @param   Tinebase_Model_Relation $_relation 
644      * @return  Tinebase_Model_Relation|NULL the new relation
645      * @throws  Tinebase_Exception_Record_Validation
646      */
647     protected function _addRelation(Tinebase_Model_Relation $_relation)
648     {
649         $_relation->created_by = Tinebase_Core::getUser()->getId();
650         $_relation->creation_time = Tinebase_DateTime::now();
651         if (!$_relation->isValid()) {
652             throw new Tinebase_Exception_Record_Validation('Relation is not valid' . print_r($_relation->getValidationErrors(),true));
653         }
654         
655         try {
656             $result = $this->_backend->addRelation($_relation);
657         } catch(Zend_Db_Statement_Exception $zse) {
658             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not add relation: ' . $zse->getMessage());
659             $result = NULL;
660         }
661         
662         return $result;
663     }
664     
665     /**
666      * update an existing relation
667      * 
668      * @param  Tinebase_Model_Relation $_relation 
669      * @return Tinebase_Model_Relation the updated relation
670      */
671     protected function _updateRelation($_relation)
672     {
673         $_relation->last_modified_by = Tinebase_Core::getUser()->getId();
674         $_relation->last_modified_time = Tinebase_DateTime::now();
675         
676         return $this->_backend->updateRelation($_relation);
677     }
678     
679     /**
680      * replaces all relations to or from a record with $sourceId to a record with $destinationId
681      * 
682      * @param string $sourceId
683      * @param string $destinationId
684      * @param string $model
685      * 
686      * @return array
687      */
688     public function transferRelations($sourceId, $destinationId, $model)
689     {
690         if (! Tinebase_Core::getUser()->hasRight('Tinebase', Tinebase_Acl_Rights::ADMIN)) {
691             throw new Tinebase_Exception_AccessDenied('Only Admins are allowed to perform his operation!');
692         }
693         
694         return $this->_backend->transferRelations($sourceId, $destinationId, $model);
695     }
696
697     /**
698      * Deletes entries
699      *
700      * @param string|integer|Tinebase_Record_Interface|array $_id
701      * @return void
702      * @return int The number of affected rows.
703      */
704     public function delete($_id)
705     {
706         return $this->_backend->delete($_id);
707     }
708
709     /**
710      * remove all relations for application
711      *
712      * @param string $applicationName
713      *
714      * @return void
715      */
716     public function removeApplication($applicationName)
717     {
718         $this->_backend->removeApplication($applicationName);
719     }
720
721     public function getRelationsOfRecordByDegree($record, $degree)
722     {
723         // get relations if not yet present OR use relation search here
724         if (empty($record->relations)) {
725             $backendType = 'Sql';
726             $modelName = get_class($record);
727             $record->relations = Tinebase_Relations::getInstance()->getRelations($modelName, $backendType, $record->getId());
728         }
729
730
731         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
732         foreach ($record->relations as $relation) {
733             if ($relation->related_degree === $degree) {
734                 $result->addRecord($relation);
735             }
736         }
737
738         return $result;
739     }
740 }