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