respect deleted attendee in event search
[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         if ($_getDeleted) {
122             $deletedFilter = new Tinebase_Model_Filter_Bool('is_deleted', 'equals', Tinebase_Model_Filter_Bool::VALUE_NOTSET);
123             $filters->addFilter($deletedFilter);
124         }
125
126         $resultSet = $this->search($filters, NULL, FALSE);
127         
128         switch (count($resultSet)) {
129             case 0: 
130                 throw new Tinebase_Exception_NotFound($this->_modelName . " record with $_property " . $_value . ' not found!');
131                 break;
132             case 1: 
133                 $result = $resultSet->getFirstRecord();
134                 break;
135             default:
136                 throw new Tinebase_Exception_UnexpectedValue(' in total ' . count($resultSet) . ' where found. But only one should!');
137         }
138         
139         return $result;
140     }
141     
142     /**
143      * Calendar optimized search function
144      * 
145      * 1. get all events neglecting grants filter
146      * 2. get all related container grants (via resolveing)
147      * 3. compute effective grants in PHP and only keep events 
148      *    user has required grant for
149      * 
150      * @TODO rethink if an outer container filter could help
151      *
152      * @param  Tinebase_Model_Filter_FilterGroup    $_filter
153      * @param  Tinebase_Model_Pagination            $_pagination
154      * @param  boolean                              $_onlyIds
155      * @return Tinebase_Record_RecordSet|array
156      */
157     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_onlyIds = FALSE)
158     {
159         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Searching events ...');
160         
161         if ($_pagination === NULL) {
162             $_pagination = new Tinebase_Model_Pagination();
163         }
164
165         $getDeleted = !!$_filter && $_filter->getFilter('is_deleted');
166         $select = parent::_getSelect('*', $getDeleted);
167         
168         $select->joinLeft(
169             /* table  */ array('exdate' => $this->_tablePrefix . 'cal_exdate'),
170             /* on     */ $this->_db->quoteIdentifier('exdate.cal_event_id') . ' = ' . $this->_db->quoteIdentifier($this->_tableName . '.id'),
171             /* select */ array('exdate' => $this->_dbCommand->getAggregate('exdate.exdate')));
172         
173         // NOTE: we join here as attendee and role filters need it
174         $select->joinLeft(
175             /* table  */ array('attendee' => $this->_tablePrefix . 'cal_attendee'),
176             /* on     */ $this->_db->quoteIdentifier('attendee.cal_event_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.id'),
177             /* select */ array());
178         // TODO move this to join?
179         $select->where($this->_db->quoteIdentifier('attendee.is_deleted') . ' = 0 OR ' . $this->_db->quoteIdentifier('attendee.is_deleted') . 'IS NULL');
180         
181         if (! $getDeleted) {
182             $select->joinLeft(
183                 /* table  */ array('dispcontainer' => $this->_tablePrefix . 'container'), 
184                 /* on     */ $this->_db->quoteIdentifier('dispcontainer.id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id'),
185                 /* select */ array());
186             
187             // TODO move this to join?
188             $select->where($this->_db->quoteIdentifier('dispcontainer.is_deleted') . ' = 0 OR ' . $this->_db->quoteIdentifier('dispcontainer.is_deleted') . 'IS NULL');
189         }
190         
191         // remove grantsfilter here as we do grants computation in PHP
192         $grantsFilter = $_filter->getFilter('grants');
193         if ($grantsFilter) {
194             $_filter->removeFilter('grants');
195         }
196         
197         // clonde the filter, as the filter is also used in the json frontend
198         // and the calendarfilter is used in the UI to
199         $clonedFilters = clone $_filter;
200         
201         $calendarFilter = null;
202         foreach ($clonedFilters as $filter) {
203             if ($filter instanceof Calendar_Model_CalendarFilter) {
204                 $calendarFilter = $filter;
205                 $clonedFilters->removeFilter($filter);
206                 break;
207             }
208         }
209         
210         $this->_addFilter($select, $clonedFilters);
211         
212         $select->group($this->_tableName . '.' . 'id');
213         Tinebase_Backend_Sql_Abstract::traitGroup($select);
214         
215         $period = $_filter->getFilter('period');
216
217         // filter out unnecessary YEARLY candidates for web client requests
218         // periods < 40 days should be client web client requests.
219         if ($period && $period->getFrom()->getClone()->addDay(40) > $period->getUntil()) {
220             $this->_removeNonMatchingBaseEvents($select, $period);
221         }
222         
223         if ($calendarFilter) {
224             $select1 = clone $select;
225             $select2 = clone $select;
226             
227             $calendarFilter->appendFilterSql1($select1, $this);
228             $calendarFilter->appendFilterSql2($select2, $this);
229             
230             $select = $this->getAdapter()->select()->union(array(
231                 $select1,
232                 $select2
233             ));
234         }
235         
236         $_pagination->appendPaginationSql($select);
237         
238         $stmt = $this->_db->query($select);
239         $rows = (array)$stmt->fetchAll(Zend_Db::FETCH_ASSOC);
240         
241         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
242                 . ' Event base rows fetched: ' . count($rows) . ' select: ' . $select);
243         
244         $result = $this->_rawDataToRecordSet($rows);
245         
246         $this->_checkGrants($result, $grantsFilter);
247         
248         return $_onlyIds ? $result->{is_bool($_onlyIds) ? $this->_getRecordIdentifier() : $_onlyIds} : $result;
249     }
250     
251     /**
252      * removes non-matching rrule base events
253      * 
254      * @param Zend_Db_Select $select
255      * @param Calendar_Model_PeriodFilter $period
256      * 
257      * TODO improve this by moving the rrules to a separate table to simplify SQL statment
258      */
259     protected function _removeNonMatchingBaseEvents($select, $period)
260     {
261         $gs = new Tinebase_Backend_Sql_Filter_GroupSelect($select);
262         $from = $period->getFrom()->getClone()->subDay(1);
263         $fromMonth = $from->format('n');
264         $fromDay = $from->format('j');
265         $until = $period->getUntil()->getClone()->addDay(1);
266         $untilMonth = $until->format('n');
267         $untilDay = $until->format('j');
268         $quotedRrule = $this->_db->quoteIdentifier('rrule');
269         
270         $gs->where($quotedRrule . ' NOT LIKE ?', 'FREQ=YEARLY%')
271         ->orWhere($quotedRrule . ' IS NULL');
272         
273         if ($fromMonth == $untilMonth && $untilDay-$fromDay <= 10) {
274             // day|week view
275             for($day=$fromDay; $day<=$untilDay; $day++) {
276                 $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$fromMonth};BYMONTHDAY={$day}%");
277             }
278         } else {
279             // monthview
280             for ($month=$fromMonth; $month<=$untilMonth; $month++) {
281                 $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$month};%");
282             }
283         }
284         
285         $gs->appendWhere(Zend_Db_Select::SQL_AND);
286         //            Tinebase_Core::getLogger()->ERR($select);
287     }
288     
289     /**
290      * calculate event permissions and remove events that don't match
291      * 
292      * @param Tinebase_Record_RecordSet $result
293      * @param Tinebase_Model_Filter_AclFilter $grantsFilter
294      */
295     protected function _checkGrants($result, $grantsFilter)
296     {
297         $clones = clone $result;
298         
299         Tinebase_Container::getInstance()->getGrantsOfRecords($clones, Tinebase_Core::getUser());
300         Calendar_Model_Attender::resolveAttendee($clones->attendee, TRUE, $clones);
301         
302         $me = Tinebase_Core::getUser()->contact_id;
303         $inheritableGrants = array(
304             Tinebase_Model_Grants::GRANT_FREEBUSY,
305             Tinebase_Model_Grants::GRANT_READ,
306             Tinebase_Model_Grants::GRANT_SYNC,
307             Tinebase_Model_Grants::GRANT_EXPORT,
308             Tinebase_Model_Grants::GRANT_PRIVATE,
309         );
310         $toRemove = array();
311         
312         foreach ($result as $event) {
313             $clone = $clones->getById($event->getId());
314             if ($event->organizer == $me) {
315                 foreach($this->_recordBasedGrants as $grant) {
316                     $event->{$grant}     = TRUE;
317                 }
318             } else {
319                 // grants to original container
320                 if ($clone->container_id instanceof Tinebase_Model_Container && $clone->container_id->account_grants) {
321                     foreach($this->_recordBasedGrants as $grant) {
322                         $event->{$grant} =     $clone->container_id->account_grants[$grant] 
323                                             || $clone->container_id->account_grants[Tinebase_Model_Grants::GRANT_ADMIN];
324                     }
325                 }
326                 
327                 // check grant inheritance
328                 foreach ($inheritableGrants as $grant) {
329                     if (! $event->{$grant} && $clone->attendee instanceof Tinebase_Record_RecordSet) {
330                         foreach($clone->attendee as $attendee) {
331                             if (   $attendee->displaycontainer_id instanceof Tinebase_Model_Container
332                                 && $attendee->displaycontainer_id->account_grants 
333                                 && (    $attendee->displaycontainer_id->account_grants[$grant]
334                                      || $attendee->displaycontainer_id->account_grants[Tinebase_Model_Grants::GRANT_ADMIN]
335                                    )
336                             ){
337                                 $event->{$grant} = TRUE;
338                                 break;
339                             }
340                         }
341                     }
342                 }
343                 
344                 if ($grantsFilter) {
345                     $requiredGrants = array_intersect($grantsFilter->getRequiredGrants(), $this->_recordBasedGrants);
346                     
347                     $hasGrant = FALSE;
348                     foreach($requiredGrants as $requiredGrant) {
349                         if ($event->{$requiredGrant}) {
350                             $hasGrant |= $event->{$requiredGrant};
351                         }
352                     }
353                     
354                     if (! $hasGrant) {
355                         $toRemove[] = $event;
356                     }
357                 }
358             }
359         }
360         
361         foreach ($toRemove as $event) {
362             $result->removeRecord($event);
363         }
364     }
365     
366     /**
367      * get the basic select object to fetch records from the database
368      *  
369      * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
370      * @param boolean $_getDeleted get deleted records (if modlog is active)
371      * @return Zend_Db_Select
372      */
373     protected function _getSelectSimple($_cols = '*', $_getDeleted = FALSE)
374     {
375         $select = $this->_db->select();
376
377         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $_cols);
378         
379         if (!$_getDeleted && $this->_modlogActive) {
380             // don't fetch deleted objects
381             $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
382         }
383         
384         return $select;
385     }
386     
387     /**
388      * Gets total count of search with $_filter
389      * 
390      * @param Tinebase_Model_Filter_FilterGroup $_filter
391      * @return int
392      */
393     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
394     {
395         $select = $this->_getSelect(array('count' => 'COUNT(*)'));
396         $this->_addFilter($select, $_filter);
397
398         $result = $this->_db->fetchOne($select);
399         
400         return $result;
401     }
402     
403     /**
404      * Updates existing entry
405      *
406      * @param Tinebase_Record_Interface $_record
407      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
408      * @return Tinebase_Record_Interface Record|NULL
409      */
410     public function update(Tinebase_Record_Interface $_record) 
411     {
412         if ($_record->rrule) {
413             $_record->rrule = (string) $_record->rrule;
414         }
415         
416         if ($_record->container_id instanceof Tinebase_Model_Container) {
417             $_record->container_id = $_record->container_id->getId();
418         }
419         
420         $_record->rrule   = !empty($_record->rrule)   ? $_record->rrule   : NULL;
421         $_record->recurid = !empty($_record->recurid) ? $_record->recurid : NULL;
422         
423         $event = parent::update($_record);
424         $this->_saveExdates($_record);
425         
426         return $this->get($event->getId(), TRUE);
427     }
428     
429     /**
430      * get the basic select object to fetch records from the database
431      *  
432      * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
433      * @param boolean $_getDeleted get deleted records (if modlog is active)
434      * @return Zend_Db_Select
435      */
436     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
437     {
438         $select = $this->_getSelectSimple();
439
440         $this->_appendEffectiveGrantCalculationSql($select);
441         
442         $select->joinLeft(
443             /* table  */ array('exdate' => $this->_tablePrefix . 'cal_exdate'), 
444             /* on     */ $this->_db->quoteIdentifier('exdate.cal_event_id') . ' = ' . $this->_db->quoteIdentifier($this->_tableName . '.id'),
445             /* select */ array('exdate' => $this->_dbCommand->getAggregate('exdate.exdate')));
446         
447         $select->group($this->_tableName . '.' . 'id');
448         
449         return $select;
450     }
451     
452     /**
453      * appends effective grant calculation to select object
454      *
455      * @param Zend_Db_Select $_select
456      */
457     protected function _appendEffectiveGrantCalculationSql($_select, $_attendeeFilters = NULL)
458     {
459         // groupmemberships of current user, needed to compute phys and inherited grants
460         $_select->joinLeft(
461             /* table  */ array('groupmemberships' => $this->_tablePrefix . 'group_members'), 
462             /* on     */ $this->_db->quoteInto($this->_db->quoteIdentifier('groupmemberships.account_id') . ' = ?' , Tinebase_Core::getUser()->getId()),
463             /* select */ array());
464         
465         // attendee joins the attendee we need to compute the curr users effective grants
466         // NOTE: 2010-04 the behaviour changed. Now, only the attendee the client filters for are 
467         //       taken into account for grants calculation 
468         $attendeeWhere = FALSE;
469         if (is_array($_attendeeFilters) && !empty($_attendeeFilters)) {
470             $attendeeSelect = $this->_db->select();
471             foreach ((array) $_attendeeFilters as $attendeeFilter) {
472                 if ($attendeeFilter instanceof Calendar_Model_AttenderFilter) {
473                     $attendeeFilter->appendFilterSql($attendeeSelect, $this);
474                 }
475             }
476             
477             $whereArray = $attendeeSelect->getPart(Zend_Db_Select::SQL_WHERE);
478             if (! empty($whereArray)) {
479                 $attendeeWhere = ' AND ' . array_value(0, $whereArray);
480             }
481         }
482         
483         $_select->joinLeft(
484             /* table  */ array('attendee' => $this->_tablePrefix . 'cal_attendee'),
485             /* on     */ $this->_db->quoteIdentifier('attendee.cal_event_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.id') . 
486                             $attendeeWhere,
487             /* select */ array());
488         
489
490             
491         $_select->joinLeft(
492             /* table  */ array('attendeeaccounts' => $this->_tablePrefix . 'accounts'), 
493             /* on     */ $this->_db->quoteIdentifier('attendeeaccounts.contact_id') . ' = ' . $this->_db->quoteIdentifier('attendee.user_id') . ' AND (' . 
494                             $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
495                             $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) . 
496                         ')',
497             /* select */ array());
498         
499         $_select->joinLeft(
500             /* table  */ array('attendeegroupmemberships' => $this->_tablePrefix . 'group_members'), 
501             /* on     */ $this->_db->quoteIdentifier('attendeegroupmemberships.account_id') . ' = ' . $this->_db->quoteIdentifier('attendeeaccounts.contact_id'),
502             /* select */ array());
503         
504
505         
506         $_select->joinLeft(
507             /* table  */ array('dispgrants' => $this->_tablePrefix . 'container_acl'), 
508             /* on     */ $this->_db->quoteIdentifier('dispgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id') . 
509                            ' AND ' . $this->_getContainGrantCondition('dispgrants', 'groupmemberships'),
510             /* select */ array());
511         
512         $_select->joinLeft(
513             /* table  */ array('physgrants' => $this->_tablePrefix . 'container_acl'), 
514             /* on     */ $this->_db->quoteIdentifier('physgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.container_id'),
515             /* select */ array());
516         
517         $allGrants = Tinebase_Model_Grants::getAllGrants();
518         
519         foreach ($allGrants as $grant) {
520             if (in_array($grant, $this->_recordBasedGrants)) {
521                 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
522                     '  /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . " OR \n" . 
523                     '  /* implicit  */' . $this->_getImplicitGrantCondition($grant) . " OR \n" .
524                     '  /* inherited */' . $this->_getInheritedGrantCondition($grant) . " \n" .
525                  ") THEN 1 ELSE 0 END ) "));
526             } else {
527                 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
528                     '  /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . "\n" .
529                 ") THEN 1 ELSE 0 END ) "));
530             }
531         }
532     }
533     
534     /**
535      * returns SQL with container grant condition 
536      *
537      * @param  string                               $_aclTableName
538      * @param  string                               $_groupMembersTableName
539      * @param  string|array                         $_requiredGrant (defaults none)
540      * @param  Zend_Db_Expr|int|Tinebase_Model_User $_user (defaults current user)
541      * @return string
542      */
543     protected function _getContainGrantCondition($_aclTableName, $_groupMembersTableName, $_requiredGrant=NULL, $_user=NULL )
544     {
545         $quoteTypeIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_type');
546         $quoteIdIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_id');
547         
548         if ($_user instanceof Zend_Db_Expr) {
549             $userExpression = $_user;
550         } else {
551             $accountId = $_user ? Tinebase_Model_User::convertUserIdToInt($_user) : Tinebase_Core::getUser()->getId();
552             $userExpression = new Zend_Db_Expr($this->_db->quote($accountId));
553         }
554         
555         $sql = $this->_db->quoteInto(    "($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_USER)  . " AND $quoteIdIdentifier = $userExpression)" .
556                $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP) . ' AND ' . $this->_db->quoteIdentifier("$_groupMembersTableName.group_id") . " = $quoteIdIdentifier" . ')' . 
557                $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?)", Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE);
558         
559         if ($_requiredGrant) {
560             $sql = "($sql) AND " . $this->_db->quoteInto($this->_db->quoteIdentifier($_aclTableName . '.account_grant') . ' IN (?)', (array)$_requiredGrant);
561             
562         }
563         
564         return "($sql)";
565     }
566     
567     /**
568      * returns SQL condition for implicit grants
569      *
570      * @param  string               $_requiredGrant
571      * @param  Tinebase_Model_User  $_user (defaults to current user)
572      * @return string
573      */
574     protected function _getImplicitGrantCondition($_requiredGrant, $_user=NULL)
575     {
576         $accountId = $_user ? $_user->getId() : Tinebase_Core::getUser()->getId();
577         $contactId = $_user ? $_user->contact_id : Tinebase_Core::getUser()->contact_id;
578         
579         // delte grant couldn't be gained implicitly
580         if ($_requiredGrant == Tinebase_Model_Grants::GRANT_DELETE) {
581             return '1=0';
582         }
583         
584         // organizer gets all other grants implicitly
585         $sql = $this->_db->quoteIdentifier('cal_events.organizer') . " = " . $this->_db->quote($contactId);
586         
587         // attendee get read, sync, export and private grants implicitly
588         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))) {
589             $readCond = $this->_db->quoteIdentifier('attendeeaccounts.id') . ' = ' . $this->_db->quote($accountId) . ' AND (' .
590                 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
591                 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) .
592             ')';
593             
594             $sql = "($sql) OR ($readCond)";
595         }
596         
597         return "($sql)";
598     }
599     
600     /**
601      * returns SQL for inherited grants
602      *
603      * @param  string $_requiredGrant
604      * @return string
605      */
606     protected function _getInheritedGrantCondition($_requiredGrant)
607     {
608         // current user needs to have grant on display calendar
609         $sql = $this->_getContainGrantCondition('dispgrants', 'groupmemberships', $_requiredGrant);
610         
611         // _AND_ attender(admin) of display calendar needs to have grant on phys calendar
612         // @todo include implicit inherited grants
613         if (! in_array($_requiredGrant, array(Tinebase_Model_Grants::GRANT_READ, Tinebase_Model_Grants::GRANT_FREEBUSY))) {
614             $userExpr = new Zend_Db_Expr($this->_db->quoteIdentifier('attendeeaccounts.id'));
615             
616             $attenderPhysGrantCond = $this->_getContainGrantCondition('physgrants', 'attendeegroupmemberships', $_requiredGrant, $userExpr);
617             // NOTE: this condition is weak! Not some attendee must have implicit grant.
618             //       -> an attender we have reqired grants for his diplay cal must have implicit grants
619             //$attenderImplicitGrantCond = $this->_getImplicitGrantCondition($_requiredGrant, $userExpr);
620             
621             //$sql = "($sql) AND ($attenderPhysGrantCond) OR ($attenderImplicitGrantCond)";
622             $sql = "($sql) AND ($attenderPhysGrantCond)";
623         }
624         
625         return "($sql)";
626     }
627     
628     /**
629      * converts raw data from adapter into a single record
630      *
631      * @param  array $_data
632      * @return Tinebase_Record_Abstract
633      */
634     protected function _rawDataToRecord(array $_rawData) {
635         $event = parent::_rawDataToRecord($_rawData);
636         
637         $this->appendForeignRecordSetToRecord($event, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
638         
639         return $event;
640     }
641     
642     /**
643      * converts raw data from adapter into a set of records
644      *
645      * @param  array $_rawData of arrays
646      * @return Tinebase_Record_RecordSet
647      */
648     protected function _rawDataToRecordSet(array $_rawData)
649     {
650         $events = new Tinebase_Record_RecordSet($this->_modelName);
651         $events->addIndices(array('rrule', 'recurid'));
652         
653         foreach ($_rawData as $rawEvent) {
654             $events->addRecord(new Calendar_Model_Event($rawEvent, true));
655         }
656         
657         $this->appendForeignRecordSetToRecordSet($events, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
658         
659         return $events;
660     }
661     
662     /**
663      * saves exdates of an event
664      *
665      * @param Calendar_Model_Event $_event
666      */
667     protected function _saveExdates($_event)
668     {
669         $this->_db->delete($this->_tablePrefix . 'cal_exdate', $this->_db->quoteInto($this->_db->quoteIdentifier('cal_event_id') . '= ?', $_event->getId()));
670         
671         // only save exdates if its an recurring event
672         if (! empty($_event->rrule)) {
673             foreach ((array)$_event->exdate as $exdate) {
674                 if (is_object($exdate)) {
675                     $this->_db->insert($this->_tablePrefix . 'cal_exdate', array(
676                         'id'           => $_event->generateUID(),
677                         'cal_event_id' => $_event->getId(),
678                         'exdate'       => $exdate->get(Tinebase_Record_Abstract::ISO8601LONG)
679                     ));
680                 } else {
681                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
682                        . ' Exdate needs to be an object:' . var_export($exdate, TRUE));
683                 }
684             }
685         }
686     }
687     
688     /**
689      * saves attendee of given event
690      * 
691      * @param Calendar_Model_Evnet $_event
692      *
693     protected function _saveAttendee($_event)
694     {
695         $attendee = $_event->attendee instanceof Tinebase_Record_RecordSet ? 
696             $_event->attendee : 
697             new Tinebase_Record_RecordSet($this->_attendeeBackend->getModelName());
698         $attendee->cal_event_id = $_event->getId();
699             
700         $currentAttendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
701         
702         $diff = $currentAttendee->getMigration($attendee->getArrayOfIds());
703         $this->_attendeeBackend->delete($diff['toDeleteIds']);
704         
705         foreach ($attendee as $attende) {
706             $method = $attende->getId() ? 'update' : 'create';
707             $this->_attendeeBackend->$method($attende);
708         }
709     }
710     */
711     
712     /****************************** attendee functions ************************/
713     
714     /**
715      * gets attendee of a given event
716      *
717      * @param Calendar_Model_Event $_event
718      * @return Tinebase_Record_RecordSet
719      */
720     public function getEventAttendee(Calendar_Model_Event $_event)
721     {
722         $attendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
723         
724         return $attendee;
725     }
726     
727     /**
728      * creates given attender in database
729      *
730      * @param Calendar_Model_Attender $_attendee
731      * @return Calendar_Model_Attender
732      */
733     public function createAttendee(Calendar_Model_Attender $_attendee)
734     {
735         if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
736             $_attendee->user_id = $_attendee->user_id->getId();
737         } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
738             $_attendee->user_id = $_attendee->user_id->group_id;
739         }
740         
741         if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
742             $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
743         }
744         
745         return $this->_attendeeBackend->create($_attendee);
746     }
747     
748     /**
749      * updates given attender in database
750      *
751      * @param Calendar_Model_Attender $_attendee
752      * @return Calendar_Model_Attender
753      */
754     public function updateAttendee(Calendar_Model_Attender $_attendee)
755     {
756         if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
757             $_attendee->user_id = $_attendee->user_id->getId();
758         } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
759             $_attendee->user_id = $_attendee->user_id->group_id;
760         }
761         
762         if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
763             $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
764         }
765         
766         return $this->_attendeeBackend->update($_attendee);
767     }
768     
769     /**
770      * deletes given attender in database
771      *
772      * @param Calendar_Model_Attender $_attendee
773      * @return void
774      */
775     public function deleteAttendee(array $_ids)
776     {
777         return $this->_attendeeBackend->delete($_ids);
778     }
779
780     /**
781      * delete duplicate events defined by an event filter
782      * 
783      * @param Calendar_Model_EventFilter $filter
784      * @param boolean $dryrun
785      * @return integer number of deleted events
786      */
787     public function deleteDuplicateEvents($filter, $dryrun = TRUE)
788     {
789         if ($dryrun && Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
790             . ' - Running in dry run mode - using filter: ' . print_r($filter->toArray(), true));
791         
792         $duplicateFields = array('summary', 'dtstart', 'dtend');
793         
794         $select = $this->_db->select();
795         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $duplicateFields);
796         $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
797             
798         $this->_addFilter($select, $filter);
799         
800         $select->group($duplicateFields)
801                ->having('count(*) > 1');
802         
803         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
804            . ' ' . $select);
805         
806         $rows = $this->_fetch($select, self::FETCH_ALL);
807         
808         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
809            . ' ' . print_r($rows, TRUE));
810         
811         $toDelete = array();
812         foreach ($rows as $row) {
813             $index = $row['summary'] . ' / ' . $row['dtstart'] . ' - ' . $row['dtend'];
814             
815             $filter = new Calendar_Model_EventFilter(array(array(
816                 'field'    => 'summary',
817                 'operator' => 'equals',
818                 'value'    => $row['summary'],
819             ), array(
820                 'field'    => 'dtstart',
821                 'operator' => 'equals',
822                 'value'    => new Tinebase_DateTime($row['dtstart']),
823             ), array(
824                 'field'    => 'dtend',
825                 'operator' => 'equals',
826                 'value'    => new Tinebase_DateTime($row['dtend']),
827             )));
828             $pagination = new Tinebase_Model_Pagination(array('sort' => array($this->_tableName . '.last_modified_time', $this->_tableName . '.creation_time'))); 
829             
830             $select = $this->_db->select();
831             $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName));
832             $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
833             
834             $this->_addFilter($select, $filter);
835             $pagination->appendPaginationSql($select);
836             
837             $rows = $this->_fetch($select, self::FETCH_ALL);
838             $events = $this->_rawDataToRecordSet($rows);
839             
840             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
841                 . ' ' . print_r($events->toArray(), TRUE));
842             
843             $deleteIds = $events->getArrayOfIds();
844             // keep the first
845             array_shift($deleteIds);
846             
847             if (! empty($deleteIds)) {
848                 $deleteContainerIds = ($events->container_id);
849                 $origContainer = array_shift($deleteContainerIds);
850                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
851                     . ' Deleting ' . count($deleteIds) . ' duplicates of: ' . $index . ' in container_ids ' . implode(',', $deleteContainerIds) . ' (origin container: ' . $origContainer . ')');
852                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
853                     . ' ' . print_r($deleteIds, TRUE));
854                 
855                 $toDelete = array_merge($toDelete, $deleteIds);
856             } else {
857                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
858                    . ' No duplicates found for ' . $index);
859             }
860         }
861         
862         if (empty($toDelete)) {
863             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
864                 . ' No duplicates found.');
865             $result = 0;
866         } else {
867             $result = ($dryrun) ? count($toDelete) : $this->delete($toDelete);
868         }
869         
870         return $result;
871     }
872     
873     /**
874      * repair dangling attendee records (no displaycontainer_id)
875      *
876      * @see https://forge.tine20.org/mantisbt/view.php?id=8172
877      */
878     public function repairDanglingDisplaycontainerEvents()
879     {
880         $filter = new Tinebase_Model_Filter_FilterGroup();
881         $filter->addFilter(new Tinebase_Model_Filter_Text(array(
882             'field'     => 'user_type',
883             'operator'  => 'in', 
884             'value'     => array(
885                 Calendar_Model_Attender::USERTYPE_USER,
886                 Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
887                 Calendar_Model_Attender::USERTYPE_RESOURCE
888             )
889         )));
890         
891         $filter->addFilter(new Tinebase_Model_Filter_Text(array(
892             'field'     => 'displaycontainer_id',
893             'operator'  => 'isnull',
894             'value'     => null
895         )));
896         
897         $danglingAttendee = $this->_attendeeBackend->search($filter);
898         $danglingContactAttendee = $danglingAttendee->filter('user_type', '/'. Calendar_Model_Attender::USERTYPE_USER . '|'. Calendar_Model_Attender::USERTYPE_GROUPMEMBER .'/', TRUE);
899         $danglingContactIds = array_unique($danglingContactAttendee->user_id);
900         $danglingContacts = Addressbook_Controller_Contact::getInstance()->getMultiple($danglingContactIds, TRUE);
901         $danglingResourceAttendee = $danglingAttendee->filter('user_type', Calendar_Model_Attender::USERTYPE_RESOURCE);
902         $danglingResourceIds =  array_unique($danglingResourceAttendee->user_id);
903         Calendar_Controller_Resource::getInstance()->doContainerACLChecks(false);
904         $danglingResources = Calendar_Controller_Resource::getInstance()->getMultiple($danglingResourceIds, TRUE);
905         
906         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
907             . ' Processing ' . count($danglingContactIds) . ' dangling contact ids...');
908         
909         foreach ($danglingContactIds as $danglingContactId) {
910             $danglingContact = $danglingContacts->getById($danglingContactId);
911             if ($danglingContact && $danglingContact->account_id) {
912                 
913                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
914                     . ' Get default display container for account ' . $danglingContact->account_id);
915                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
916                     . ' ' . print_r($danglingContact->toArray(), true));
917                 
918                 $displayCalId = Calendar_Controller_Event::getDefaultDisplayContainerId($danglingContact->account_id);
919                 if ($displayCalId) {
920                     // finaly repair attendee records
921                     $attendeeRecords = $danglingContactAttendee->filter('user_id', $danglingContactId);
922                     $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
923                     Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following contact attendee " . print_r($attendeeRecords->toArray(), TRUE));
924                 }
925             }
926         }
927         
928         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
929             . ' Processing ' . count($danglingResourceIds) . ' dangling resource ids...');
930         
931         foreach ($danglingResourceIds as $danglingResourceId) {
932             $resource = $danglingResources->getById($danglingResourceId);
933             if ($resource && $resource->container_id) {
934                 $displayCalId = $resource->container_id;
935                 $attendeeRecords = $danglingResourceAttendee->filter('user_id', $danglingResourceId);
936                 $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
937                 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following resource attendee " . print_r($attendeeRecords->toArray(), TRUE));
938             }
939         }
940     }
941 }