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