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