ae528652512ab7d93fa9fd5f6ecca9b3c6475219
[tine20] / tine20 / Calendar / Backend / Sql.php
1 <?php
2 /**
3  * Sql Calendar 
4  * 
5  * @package     Calendar
6  * @subpackage  Backend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2010-2013 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * Native tine 2.0 events sql backend
14  *
15  * Events consists of the properties of Calendar_Model_Event except Tags and Notes 
16  * which are as always handles by their controllers/backends
17  * 
18  * @TODO rework fetch handling. all fetch operations should be based on search.
19  *       remove old grant sql when done
20  * 
21  * @package     Calendar 
22  * @subpackage  Backend
23  */
24 class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
25 {
26     /**
27      * Table name without prefix
28      *
29      * @var string
30      */
31     protected $_tableName = 'cal_events';
32     
33     /**
34      * Model name
35      *
36      * @var string
37      */
38     protected $_modelName = 'Calendar_Model_Event';
39     
40     /**
41      * if modlog is active, we add 'is_deleted = 0' to select object in _getSelect()
42      *
43      * @var boolean
44      */
45     protected $_modlogActive = TRUE;
46     
47     /**
48      * attendee backend
49      * 
50      * @var Calendar_Backend_Sql_Attendee
51      */
52     protected $_attendeeBackend = NULL;
53     
54     /**
55      * list of record based grants
56      */
57     protected $_recordBasedGrants = array(
58         Tinebase_Model_Grants::GRANT_FREEBUSY,
59         Tinebase_Model_Grants::GRANT_READ, 
60         Tinebase_Model_Grants::GRANT_SYNC, 
61         Tinebase_Model_Grants::GRANT_EXPORT, 
62         Tinebase_Model_Grants::GRANT_EDIT, 
63         Tinebase_Model_Grants::GRANT_DELETE, 
64         Tinebase_Model_Grants::GRANT_PRIVATE,
65     );
66     
67     /**
68      * the constructor
69      *
70      * @param Zend_Db_Adapter_Abstract $_db optional
71      * @param array $_options (optional)
72      */
73     public function __construct ($_dbAdapter = NULL, $_options = array())
74     {
75         parent::__construct($_dbAdapter, $_options);
76         
77         $this->_attendeeBackend = new Calendar_Backend_Sql_Attendee($_dbAdapter);
78     }
79     
80     /**
81      * Creates new entry
82      *
83      * @param   Tinebase_Record_Interface $_record
84      * @return  Tinebase_Record_Interface
85      * @throws  Tinebase_Exception_InvalidArgument
86      * @throws  Tinebase_Exception_UnexpectedValue
87      */
88     public function create(Tinebase_Record_Interface $_record) 
89     {
90         
91         if ($_record->rrule) {
92             $_record->rrule = (string) $_record->rrule;
93         }
94         $_record->rrule   = !empty($_record->rrule)   ? $_record->rrule   : NULL;
95         $_record->recurid = !empty($_record->recurid) ? $_record->recurid : NULL;
96         
97         $event = parent::create($_record);
98         $this->_saveExdates($_record);
99         //$this->_saveAttendee($_record);
100         
101         return $this->get($event->getId());
102     }
103     
104     /**
105      * Gets one entry (by property)
106      *
107      * @param  mixed  $_value
108      * @param  string $_property
109      * @param  bool   $_getDeleted
110      * @return Tinebase_Record_Interface
111      * @throws Tinebase_Exception_NotFound
112      */
113     public function getByProperty($_value, $_property = 'name', $_getDeleted = FALSE) 
114     {
115         //$pagination = new Tinebase_Model_Pagination(array('limit' => 1));
116         $filters = new Calendar_Model_EventFilter();
117         
118         $filter = new Tinebase_Model_Filter_Text($_property, 'equals', $_value);
119         $filters->addFilter($filter);
120         
121         $resultSet = $this->search($filters, NULL, FALSE, $_getDeleted);
122         
123         switch (count($resultSet)) {
124             case 0: 
125                 throw new Tinebase_Exception_NotFound($this->_modelName . " record with $_property " . $_value . ' not found!');
126                 break;
127             case 1: 
128                 $result = $resultSet->getFirstRecord();
129                 break;
130             default:
131                 throw new Tinebase_Exception_UnexpectedValue(' in total ' . count($resultSet) . ' where found. But only one should!');
132         }
133         
134         return $result;
135     }
136     
137     /**
138      * Calendar optimized search function
139      * 
140      * 1. get all events neglecting grants filter
141      * 2. get all related container grants (via resolveing)
142      * 3. compute effective grants in PHP and only keep events 
143      *    user has required grant for
144      * 
145      * @TODO rethink if an outer container filter could help
146      *
147      * @param  Tinebase_Model_Filter_FilterGroup    $_filter
148      * @param  Tinebase_Model_Pagination            $_pagination
149      * @param  boolean                              $_onlyIds
150      * @param  bool   $_getDeleted
151      * @return Tinebase_Record_RecordSet|array
152      */
153     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_onlyIds = FALSE, $_getDeleted = FALSE)    
154     {
155         if ($_pagination === NULL) {
156             $_pagination = new Tinebase_Model_Pagination();
157         }
158         
159         $select = parent::_getSelect('*', $_getDeleted);
160         
161         $select->joinLeft(
162             /* table  */ array('exdate' => $this->_tablePrefix . 'cal_exdate'),
163             /* on     */ $this->_db->quoteIdentifier('exdate.cal_event_id') . ' = ' . $this->_db->quoteIdentifier($this->_tableName . '.id'),
164             /* select */ array('exdate' => $this->_dbCommand->getAggregate('exdate.exdate')));
165         
166         $select->joinLeft(
167             /* table  */ array('attendee' => $this->_tablePrefix . 'cal_attendee'),
168             /* on     */ $this->_db->quoteIdentifier('attendee.cal_event_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.id'),
169             /* select */ array());
170         
171         if (! $_getDeleted) {
172             $select->joinLeft(
173                 /* table  */ array('dispcontainer' => $this->_tablePrefix . 'container'), 
174                 /* on     */ $this->_db->quoteIdentifier('dispcontainer.id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id'),
175                 /* select */ array());
176             
177             $select->where($this->_db->quoteIdentifier('dispcontainer.is_deleted') . ' = 0 OR ' . $this->_db->quoteIdentifier('dispcontainer.is_deleted') . 'IS NULL');
178         }
179         
180         // remove grantsfilter here as we do grants computation in PHP
181         $grantsFilter = $_filter->getFilter('grants');
182         if ($grantsFilter) {
183             $_filter->removeFilter('grants');
184         }
185         
186         $this->_addFilter($select, $_filter);
187         $_pagination->appendPaginationSql($select);
188         
189         $select->group($this->_tableName . '.' . 'id');
190         Tinebase_Backend_Sql_Abstract::traitGroup($select);
191         
192         $stmt = $this->_db->query($select);
193         $rows = (array)$stmt->fetchAll(Zend_Db::FETCH_ASSOC);
194         $result = $this->_rawDataToRecordSet($rows);
195         $clones = clone $result;
196         
197         Tinebase_Container::getInstance()->getGrantsOfRecords($clones, Tinebase_Core::getUser());
198         Calendar_Model_Attender::resolveAttendee($clones->attendee, TRUE, $clones);
199         
200         $me = Tinebase_Core::getUser()->contact_id;
201         $inheritableGrants = array(
202             Tinebase_Model_Grants::GRANT_FREEBUSY,
203             Tinebase_Model_Grants::GRANT_READ, 
204             Tinebase_Model_Grants::GRANT_SYNC, 
205             Tinebase_Model_Grants::GRANT_EXPORT, 
206             Tinebase_Model_Grants::GRANT_PRIVATE,
207         );
208         $toRemove = array();
209         
210         foreach($result as $event) {
211             $clone = $clones->getById($event->getId());
212             if ($event->organizer == $me) {
213                 foreach($this->_recordBasedGrants as $grant) {
214                     $event->{$grant}     = TRUE;
215                 }
216             } else {
217                 // grants to original container
218                 if ($clone->container_id instanceof Tinebase_Model_Container && $clone->container_id->account_grants) {
219                     foreach($this->_recordBasedGrants as $grant) {
220                         $event->{$grant} =     $clone->container_id->account_grants[$grant] 
221                                             || $clone->container_id->account_grants[Tinebase_Model_Grants::GRANT_ADMIN];
222                     }
223                 }
224                 
225                 // check grant inheritance
226                 foreach($inheritableGrants as $grant) {
227                     if (! $event->{$grant} && $clone->attendee instanceof Tinebase_Record_RecordSet) {
228                         foreach($clone->attendee as $attendee) {
229                             if (   $attendee->displaycontainer_id instanceof Tinebase_Model_Container
230                                 && $attendee->displaycontainer_id->account_grants 
231                                 && (    $attendee->displaycontainer_id->account_grants[$grant]
232                                      || $attendee->displaycontainer_id->account_grants[Tinebase_Model_Grants::GRANT_ADMIN]
233                                    )
234                             ){
235                                 $event->{$grant} = TRUE;
236                                 break;
237                             }
238                         }
239                     }
240                 }
241                 
242                 if ($grantsFilter) {
243                     $requiredGrants = array_intersect($grantsFilter->getRequiredGrants(), $this->_recordBasedGrants);
244                     
245                     $hasGrant = FALSE;
246                     foreach($requiredGrants as $requiredGrant) {
247                         if ($event->{$requiredGrant}) {
248                             $hasGrant |= $event->{$requiredGrant};
249                         }
250                     }
251                     
252                     if (! $hasGrant) {
253                         $toRemove[] = $event;
254                     }
255                 }
256             }
257         }
258         
259         foreach ($toRemove as $event) {
260             $result->removeRecord($event);
261         }
262         
263         return $_onlyIds ? $result->{is_bool($_onlyIds) ? $this->_getRecordIdentifier() : $_onlyIds} : $result;
264     }
265     
266     /**
267      * get the basic select object to fetch records from the database
268      *  
269      * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
270      * @param boolean $_getDeleted get deleted records (if modlog is active)
271      * @return Zend_Db_Select
272      */
273     protected function _getSelectSimple($_cols = '*', $_getDeleted = FALSE)
274     {
275         $select = $this->_db->select();
276
277         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $_cols);
278         
279         if (!$_getDeleted && $this->_modlogActive) {
280             // don't fetch deleted objects
281             $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
282         }
283         
284         return $select;
285     }
286     
287     /**
288      * Gets total count of search with $_filter
289      * 
290      * @param Tinebase_Model_Filter_FilterGroup $_filter
291      * @return int
292      */
293     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
294     {
295         $select = $this->_getSelect(array('count' => 'COUNT(*)'));
296         $this->_addFilter($select, $_filter);
297
298         $result = $this->_db->fetchOne($select);
299         
300         return $result;
301     }    
302     
303     /**
304      * Updates existing entry
305      *
306      * @param Tinebase_Record_Interface $_record
307      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
308      * @return Tinebase_Record_Interface Record|NULL
309      */
310     public function update(Tinebase_Record_Interface $_record) 
311     {
312         if ($_record->rrule) {
313             $_record->rrule = (string) $_record->rrule;
314         }
315         
316         if ($_record->container_id instanceof Tinebase_Model_Container) {
317             $_record->container_id = $_record->container_id->getId();
318         }
319         
320         $_record->rrule   = !empty($_record->rrule)   ? $_record->rrule   : NULL;
321         $_record->recurid = !empty($_record->recurid) ? $_record->recurid : NULL;
322         
323         $event = parent::update($_record);
324         $this->_saveExdates($_record);
325         //$this->_saveAttendee($_record);
326         
327         return $this->get($event->getId(), TRUE);
328     }
329     
330     /**
331      * get the basic select object to fetch records from the database
332      *  
333      * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
334      * @param boolean $_getDeleted get deleted records (if modlog is active)
335      * @return Zend_Db_Select
336      */
337     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
338     {
339         $select = $this->_getSelectSimple();
340
341         $this->_appendEffectiveGrantCalculationSql($select);
342         
343         $select->joinLeft(
344             /* table  */ array('exdate' => $this->_tablePrefix . 'cal_exdate'), 
345             /* on     */ $this->_db->quoteIdentifier('exdate.cal_event_id') . ' = ' . $this->_db->quoteIdentifier($this->_tableName . '.id'),
346             /* select */ array('exdate' => $this->_dbCommand->getAggregate('exdate.exdate')));
347         
348         $select->group($this->_tableName . '.' . 'id');
349         
350         return $select;
351     }
352     
353     /**
354      * appends effective grant calculation to select object
355      *
356      * @param Zend_Db_Select $_select
357      */
358     protected function _appendEffectiveGrantCalculationSql($_select, $_attendeeFilters = NULL)
359     {
360         // groupmemberships of current user, needed to compute phys and inherited grants
361         $_select->joinLeft(
362             /* table  */ array('groupmemberships' => $this->_tablePrefix . 'group_members'), 
363             /* on     */ $this->_db->quoteInto($this->_db->quoteIdentifier('groupmemberships.account_id') . ' = ?' , Tinebase_Core::getUser()->getId()),
364             /* select */ array());
365         
366         // attendee joins the attendee we need to compute the curr users effective grants
367         // NOTE: 2010-04 the behaviour changed. Now, only the attendee the client filters for are 
368         //       taken into account for grants calculation 
369         $attendeeWhere = FALSE;
370         if (is_array($_attendeeFilters) && !empty($_attendeeFilters)) {
371             $attendeeSelect = $this->_db->select();
372             foreach ((array) $_attendeeFilters as $attendeeFilter) {
373                 if ($attendeeFilter instanceof Calendar_Model_AttenderFilter) {
374                     $attendeeFilter->appendFilterSql($attendeeSelect, $this);
375                 }
376             }
377             
378             $whereArray = $attendeeSelect->getPart(Zend_Db_Select::SQL_WHERE);
379             if (! empty($whereArray)) {
380                 $attendeeWhere = ' AND ' . array_value(0, $whereArray);
381             }
382         }
383         
384         $_select->joinLeft(
385             /* table  */ array('attendee' => $this->_tablePrefix . 'cal_attendee'),
386             /* on     */ $this->_db->quoteIdentifier('attendee.cal_event_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.id') . 
387                             $attendeeWhere,
388             /* select */ array());
389         
390
391             
392         $_select->joinLeft(
393             /* table  */ array('attendeeaccounts' => $this->_tablePrefix . 'accounts'), 
394             /* on     */ $this->_db->quoteIdentifier('attendeeaccounts.contact_id') . ' = ' . $this->_db->quoteIdentifier('attendee.user_id') . ' AND (' . 
395                             $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
396                             $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) . 
397                         ')',
398             /* select */ array());
399         
400         $_select->joinLeft(
401             /* table  */ array('attendeegroupmemberships' => $this->_tablePrefix . 'group_members'), 
402             /* on     */ $this->_db->quoteIdentifier('attendeegroupmemberships.account_id') . ' = ' . $this->_db->quoteIdentifier('attendeeaccounts.contact_id'),
403             /* select */ array());
404         
405
406         
407         $_select->joinLeft(
408             /* table  */ array('dispgrants' => $this->_tablePrefix . 'container_acl'), 
409             /* on     */ $this->_db->quoteIdentifier('dispgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id') . 
410                            ' AND ' . $this->_getContainGrantCondition('dispgrants', 'groupmemberships'),
411             /* select */ array());
412         
413         $_select->joinLeft(
414             /* table  */ array('physgrants' => $this->_tablePrefix . 'container_acl'), 
415             /* on     */ $this->_db->quoteIdentifier('physgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.container_id'),
416             /* select */ array());
417         
418         $allGrants = Tinebase_Model_Grants::getAllGrants();
419         
420         foreach ($allGrants as $grant) {
421             if (in_array($grant, $this->_recordBasedGrants)) {
422                 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
423                     '  /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . " OR \n" . 
424                     '  /* implicit  */' . $this->_getImplicitGrantCondition($grant) . " OR \n" .
425                     '  /* inherited */' . $this->_getInheritedGrantCondition($grant) . " \n" .
426                  ") THEN 1 ELSE 0 END ) "));
427             } else {
428                 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
429                     '  /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . "\n" .
430                 ") THEN 1 ELSE 0 END ) "));
431             }
432         }
433     }
434     
435     /**
436      * returns SQL with container grant condition 
437      *
438      * @param  string                               $_aclTableName
439      * @param  string                               $_groupMembersTableName
440      * @param  string|array                         $_requiredGrant (defaults none)
441      * @param  Zend_Db_Expr|int|Tinebase_Model_User $_user (defaults current user)
442      * @return string
443      */
444     protected function _getContainGrantCondition($_aclTableName, $_groupMembersTableName, $_requiredGrant=NULL, $_user=NULL )
445     {
446         $quoteTypeIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_type');
447         $quoteIdIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_id');
448         
449         if ($_user instanceof Zend_Db_Expr) {
450             $userExpression = $_user;
451         } else {
452             $accountId = $_user ? Tinebase_Model_User::convertUserIdToInt($_user) : Tinebase_Core::getUser()->getId();
453             $userExpression = new Zend_Db_Expr($this->_db->quote($accountId));
454         }
455         
456         $sql = $this->_db->quoteInto(    "($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_USER)  . " AND $quoteIdIdentifier = $userExpression)" .
457                $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP) . ' AND ' . $this->_db->quoteIdentifier("$_groupMembersTableName.group_id") . " = $quoteIdIdentifier" . ')' . 
458                $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?)", Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE);
459         
460         if ($_requiredGrant) {
461             $sql = "($sql) AND " . $this->_db->quoteInto($this->_db->quoteIdentifier($_aclTableName . '.account_grant') . ' IN (?)', (array)$_requiredGrant);
462             
463         }
464         
465         return "($sql)";
466     }
467     
468     /**
469      * returns SQL condition for implicit grants
470      *
471      * @param  string               $_requiredGrant
472      * @param  Tinebase_Model_User  $_user (defaults to current user)
473      * @return string
474      */
475     protected function _getImplicitGrantCondition($_requiredGrant, $_user=NULL)
476     {
477         $accountId = $_user ? $_user->getId() : Tinebase_Core::getUser()->getId();
478         $contactId = $_user ? $user->contact_id : Tinebase_Core::getUser()->contact_id;
479         
480         // delte grant couldn't be gained implicitly
481         if ($_requiredGrant == Tinebase_Model_Grants::GRANT_DELETE) {
482             return '1=0';
483         }
484         
485         // organizer gets all other grants implicitly
486         $sql = $this->_db->quoteIdentifier('cal_events.organizer') . " = " . $this->_db->quote($contactId);
487         
488         // attendee get read, sync, export and private grants implicitly
489         if (in_array($_requiredGrant, array(Tinebase_Model_Grants::GRANT_READ, Tinebase_Model_Grants::GRANT_SYNC, Tinebase_Model_Grants::GRANT_EXPORT, Tinebase_Model_Grants::GRANT_PRIVATE))) {
490             $readCond = $this->_db->quoteIdentifier('attendeeaccounts.id') . ' = ' . $this->_db->quote($accountId) . ' AND (' .
491                 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
492                 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) .
493             ')';
494             
495             $sql = "($sql) OR ($readCond)";
496         }
497         
498         return "($sql)";
499     }
500     
501     /**
502      * returns SQL for inherited grants
503      *
504      * @param  string $_requiredGrant
505      * @return string
506      */
507     protected function _getInheritedGrantCondition($_requiredGrant)
508     {
509         // current user needs to have grant on display calendar
510         $sql = $this->_getContainGrantCondition('dispgrants', 'groupmemberships', $_requiredGrant);
511         
512         // _AND_ attender(admin) of display calendar needs to have grant on phys calendar
513         // @todo include implicit inherited grants
514         if (! in_array($_requiredGrant, array(Tinebase_Model_Grants::GRANT_READ, Tinebase_Model_Grants::GRANT_FREEBUSY))) {
515             $userExpr = new Zend_Db_Expr($this->_db->quoteIdentifier('attendeeaccounts.id'));
516             
517             $attenderPhysGrantCond = $this->_getContainGrantCondition('physgrants', 'attendeegroupmemberships', $_requiredGrant, $userExpr);
518             // NOTE: this condition is weak! Not some attendee must have implicit grant.
519             //       -> an attender we have reqired grants for his diplay cal must have implicit grants
520             //$attenderImplicitGrantCond = $this->_getImplicitGrantCondition($_requiredGrant, $userExpr);
521             
522             //$sql = "($sql) AND ($attenderPhysGrantCond) OR ($attenderImplicitGrantCond)";
523             $sql = "($sql) AND ($attenderPhysGrantCond)";
524         }
525         
526         return "($sql)";
527     }
528     
529     /**
530      * converts raw data from adapter into a single record
531      *
532      * @param  array $_data
533      * @return Tinebase_Record_Abstract
534      */
535     protected function _rawDataToRecord(array $_rawData) {
536         $event = parent::_rawDataToRecord($_rawData);
537         
538         $this->appendForeignRecordSetToRecord($event, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
539         
540         return $event;
541     }
542     
543     /**
544      * converts raw data from adapter into a set of records
545      *
546      * @param  array $_rawData of arrays
547      * @return Tinebase_Record_RecordSet
548      */
549     protected function _rawDataToRecordSet(array $_rawData)
550     {
551         $events = new Tinebase_Record_RecordSet($this->_modelName);
552         $events->addIndices(array('rrule', 'recurid'));
553         
554         foreach ($_rawData as $rawEvent) {
555             $events->addRecord(new Calendar_Model_Event($rawEvent, true));
556         }
557         
558         $this->appendForeignRecordSetToRecordSet($events, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
559         
560         return $events;
561     }
562     
563     /**
564      * saves exdates of an event
565      *
566      * @param Calendar_Model_Event $_event
567      */
568     protected function _saveExdates($_event)
569     {
570         $this->_db->delete($this->_tablePrefix . 'cal_exdate', $this->_db->quoteInto($this->_db->quoteIdentifier('cal_event_id') . '= ?', $_event->getId()));
571         
572         // only save exdates if its an recurring event
573         if (! empty($_event->rrule)) {
574             foreach ((array)$_event->exdate as $exdate) {
575                 if (is_object($exdate)) {
576                     $this->_db->insert($this->_tablePrefix . 'cal_exdate', array(
577                         'id'           => $_event->generateUID(),
578                         'cal_event_id' => $_event->getId(),
579                         'exdate'       => $exdate->get(Tinebase_Record_Abstract::ISO8601LONG)
580                     ));
581                 } else {
582                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
583                        . ' Exdate needs to be an object:' . var_export($exdate, TRUE));
584                 }
585             }
586         }
587     }
588     
589     /**
590      * saves attendee of given event
591      * 
592      * @param Calendar_Model_Evnet $_event
593      *
594     protected function _saveAttendee($_event)
595     {
596         $attendee = $_event->attendee instanceof Tinebase_Record_RecordSet ? 
597             $_event->attendee : 
598             new Tinebase_Record_RecordSet($this->_attendeeBackend->getModelName());
599         $attendee->cal_event_id = $_event->getId();
600             
601         $currentAttendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
602         
603         $diff = $currentAttendee->getMigration($attendee->getArrayOfIds());
604         $this->_attendeeBackend->delete($diff['toDeleteIds']);
605         
606         foreach ($attendee as $attende) {
607             $method = $attende->getId() ? 'update' : 'create';
608             $this->_attendeeBackend->$method($attende);
609         }
610     }
611     */
612     
613     /****************************** attendee functions ************************/
614     
615     /**
616      * gets attendee of a given event
617      *
618      * @param Calendar_Model_Event $_event
619      * @return Tinebase_Record_RecordSet
620      */
621     public function getEventAttendee(Calendar_Model_Event $_event)
622     {
623         $attendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
624         
625         return $attendee;
626     }
627     
628     /**
629      * creates given attender in database
630      *
631      * @param Calendar_Model_Attender $_attendee
632      * @return Calendar_Model_Attender
633      */
634     public function createAttendee(Calendar_Model_Attender $_attendee)
635     {
636         if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
637             $_attendee->user_id = $_attendee->user_id->getId();
638         } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
639             $_attendee->user_id = $_attendee->user_id->group_id;
640         }
641         
642         if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
643             $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
644         }
645         
646         return $this->_attendeeBackend->create($_attendee);
647     }
648     
649     /**
650      * updates given attender in database
651      *
652      * @param Calendar_Model_Attender $_attendee
653      * @return Calendar_Model_Attender
654      */
655     public function updateAttendee(Calendar_Model_Attender $_attendee)
656     {
657         if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
658             $_attendee->user_id = $_attendee->user_id->getId();
659         } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
660             $_attendee->user_id = $_attendee->user_id->group_id;
661         }
662         
663         if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
664             $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
665         }
666         
667         return $this->_attendeeBackend->update($_attendee);
668     }
669     
670     /**
671      * deletes given attender in database
672      *
673      * @param Calendar_Model_Attender $_attendee
674      * @return void
675      */
676     public function deleteAttendee(array $_ids)
677     {
678         return $this->_attendeeBackend->delete($_ids);
679     }
680
681     /**
682      * delete duplicate events defined by an event filter
683      * 
684      * @param Calendar_Model_EventFilter $filter
685      * @param boolean $dryrun
686      * @return integer number of deleted events
687      */
688     public function deleteDuplicateEvents($filter, $dryrun = TRUE)
689     {
690         if ($dryrun && Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
691             . ' - Running in dry run mode -');
692                 
693         $duplicateFields = array('summary', 'dtstart', 'dtend');
694         
695         $select = $this->_db->select();
696         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $duplicateFields);
697         $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
698             
699         $this->_addFilter($select, $filter);
700         
701         $select->group($duplicateFields)
702                ->having('count(*) > 1');
703         
704         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
705            . ' ' . $select);
706         
707         $rows = $this->_fetch($select, self::FETCH_ALL);
708         
709         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
710            . ' ' . print_r($rows, TRUE));
711         
712         $toDelete = array();
713         foreach ($rows as $row) {
714             $index = $row['summary'] . ' / ' . $row['dtstart'] . ' - ' . $row['dtend'];
715             
716             $events = $this->search(new Calendar_Model_EventFilter(array(array(
717                 'field'    => 'summary',
718                 'operator' => 'equals',
719                 'value'    => $row['summary'],
720             ), array(
721                 'field'    => 'dtstart',
722                 'operator' => 'equals',
723                 'value'    => new Tinebase_DateTime($row['dtstart']),
724             ), array(
725                 'field'    => 'dtend',
726                 'operator' => 'equals',
727                 'value'    => new Tinebase_DateTime($row['dtend']),
728             ))), new Tasks_Model_Pagination(array('sort' => $this->_tableName . '.creation_time')));
729             
730             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
731                 . ' ' . print_r($events->toArray(), TRUE));
732             
733             $deleteIds = $events->getArrayOfIds();
734             // keep the first
735             array_shift($deleteIds);
736             
737             if (! empty($deleteIds)) {
738                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
739                     . ' Deleting ' . count($deleteIds) . ' duplicates of: ' . $index);
740                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
741                     . ' ' . print_r($deleteIds, TRUE));
742                 
743                 $toDelete = array_merge($toDelete, $deleteIds);
744             } else {
745                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
746                    . ' No duplicates found for ' . $index);
747             }
748         }
749         
750         if (empty($toDelete)) {
751             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
752                 . ' No duplicates found.');
753             $result = 0;
754         } else {
755             $result = ($dryrun) ? count($toDelete) : $this->delete($toDelete);
756         }
757         
758         return $result;
759     }
760     
761     /**
762      * repair dangling attendee records (no displaycontainer_id)
763      *
764      * @see https://forge.tine20.org/mantisbt/view.php?id=8172
765      */
766     public function repairDanglingDisplaycontainerEvents()
767     {
768         $filter = new Tinebase_Model_Filter_FilterGroup();
769         $filter->addFilter(new Tinebase_Model_Filter_Text(array(
770             'field'     => 'user_type',
771             'operator'  => 'in', 
772             'value'     => array(
773                 Calendar_Model_Attender::USERTYPE_USER,
774                 Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
775                 Calendar_Model_Attender::USERTYPE_RESOURCE
776             )
777         )));
778         
779         $filter->addFilter(new Tinebase_Model_Filter_Text(array(
780             'field'     => 'displaycontainer_id',
781             'operator'  => 'isnull',
782             'value'     => null
783         )));
784         
785         $danglingAttendee = $this->_attendeeBackend->search($filter);
786         $danglingContactAttendee = $danglingAttendee->filter('user_type', '/'. Calendar_Model_Attender::USERTYPE_USER . '|'. Calendar_Model_Attender::USERTYPE_GROUPMEMBER .'/', TRUE);
787         $danglingContactIds = array_unique($danglingContactAttendee->user_id);
788         $danglingContacts = Addressbook_Controller_Contact::getInstance()->getMultiple($danglingContactIds, TRUE);
789         $danglingResourceAttendee = $danglingAttendee->filter('user_type', Calendar_Model_Attender::USERTYPE_RESOURCE);
790         $danglingResourceIds =  array_unique($danglingResourceAttendee->user_id);
791         Calendar_Controller_Resource::getInstance()->doContainerACLChecks(false);
792         $danglingResources = Calendar_Controller_Resource::getInstance()->getMultiple($danglingResourceIds, TRUE);
793         
794         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
795             . ' Processing ' . count($danglingContactIds) . ' dangling contact ids...');
796         
797         foreach ($danglingContactIds as $danglingContactId) {
798             $danglingContact = $danglingContacts->getById($danglingContactId);
799             if ($danglingContact && $danglingContact->account_id) {
800                 
801                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
802                     . ' Get default display container for account ' . $danglingContact->account_id);
803                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
804                     . ' ' . print_r($danglingContact->toArray(), true));
805                 
806                 $displayCalId = Calendar_Controller_Event::getDefaultDisplayContainerId($danglingContact->account_id);
807                 if ($displayCalId) {
808                     // finaly repair attendee records
809                     $attendeeRecords = $danglingContactAttendee->filter('user_id', $danglingContactId);
810                     $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
811                     Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following contact attendee " . print_r($attendeeRecords->toArray(), TRUE));
812                 }
813             }
814         }
815         
816         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
817             . ' Processing ' . count($danglingResourceIds) . ' dangling resource ids...');
818         
819         foreach ($danglingResourceIds as $danglingResourceId) {
820             $resource = $danglingResources->getById($danglingResourceId);
821             if ($resource && $resource->container_id) {
822                 $displayCalId = $resource->container_id;
823                 $attendeeRecords = $danglingResourceAttendee->filter('user_id', $danglingResourceId);
824                 $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
825                 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following resource attendee " . print_r($attendeeRecords->toArray(), TRUE));
826             }
827         }
828     }
829     
830     /**
831      * sets etags, expects ids as keys and etags as value
832      * 
833      * @param array $etags
834      */
835     public function setETags(array $etags)
836     {
837         foreach ($etags as $id => $etag) {
838             //$etag = replaceSpecialChars($etag);
839             
840             $where  = array(
841                 $this->_db->quoteInto($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id),
842             );
843             $this->_db->update($this->_tablePrefix . $this->_tableName, array('etag' => $etag), $where);
844         }
845     }
846     
847     /**
848      * checks if there is an event with this id and etag, or an event with the same id 
849      * 
850      * @param string $id
851      * @param string $etag
852      * @return boolean
853      * @throws Tinebase_Exception_NotFound
854      */
855     public function checkETag($id, $etag)
856     {
857         //$etag = replaceSpecialChars($etag);
858         
859         $select = $this->_db->select();
860         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $this->_identifier);
861         $select->where($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id);
862         
863         $stmt = $select->query();
864         $queryResult = $stmt->fetch();
865         $stmt->closeCursor();
866         
867         if ($queryResult === false) {
868             throw new Tinebase_Exception_NotFound('no event with id ' . $id .' found');
869         }
870         
871         $select->where($this->_db->quoteIdentifier('etag') . ' = ?', $etag);
872         $stmt = $select->query();
873         $queryResult = $stmt->fetch();
874         $stmt->closeCursor();
875         
876         return ($queryResult !== false);
877     }
878 }