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)
13 * Native tine 2.0 events sql backend
15 * Events consists of the properties of Calendar_Model_Event except Tags and Notes
16 * which are as always handles by their controllers/backends
18 * @TODO rework fetch handling. all fetch operations should be based on search.
19 * remove old grant sql when done
24 class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
27 * Table name without prefix
31 protected $_tableName = 'cal_events';
38 protected $_modelName = 'Calendar_Model_Event';
41 * if modlog is active, we add 'is_deleted = 0' to select object in _getSelect()
45 protected $_modlogActive = TRUE;
50 * @var Calendar_Backend_Sql_Attendee
52 protected $_attendeeBackend = NULL;
55 * list of record based grants
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,
70 * @param Zend_Db_Adapter_Abstract $_db optional
71 * @param array $_options (optional)
73 public function __construct ($_dbAdapter = NULL, $_options = array())
75 parent::__construct($_dbAdapter, $_options);
77 $this->_attendeeBackend = new Calendar_Backend_Sql_Attendee($_dbAdapter);
83 * @param Tinebase_Record_Interface $_record
84 * @return Tinebase_Record_Interface
85 * @throws Tinebase_Exception_InvalidArgument
86 * @throws Tinebase_Exception_UnexpectedValue
88 public function create(Tinebase_Record_Interface $_record)
91 if ($_record->rrule) {
92 $_record->rrule = (string) $_record->rrule;
94 $_record->rrule = !empty($_record->rrule) ? $_record->rrule : NULL;
95 $_record->recurid = !empty($_record->recurid) ? $_record->recurid : NULL;
97 $event = parent::create($_record);
98 $this->_saveExdates($_record);
99 //$this->_saveAttendee($_record);
101 return $this->get($event->getId());
105 * Gets one entry (by property)
107 * @param mixed $_value
108 * @param string $_property
109 * @param bool $_getDeleted
110 * @return Tinebase_Record_Interface
111 * @throws Tinebase_Exception_NotFound
113 public function getByProperty($_value, $_property = 'name', $_getDeleted = FALSE)
115 //$pagination = new Tinebase_Model_Pagination(array('limit' => 1));
116 $filters = new Calendar_Model_EventFilter();
118 $filter = new Tinebase_Model_Filter_Text($_property, 'equals', $_value);
119 $filters->addFilter($filter);
122 $deletedFilter = new Tinebase_Model_Filter_Bool('is_deleted', 'equals', Tinebase_Model_Filter_Bool::VALUE_NOTSET);
123 $filters->addFilter($deletedFilter);
126 $resultSet = $this->search($filters, NULL, FALSE);
128 switch (count($resultSet)) {
130 throw new Tinebase_Exception_NotFound($this->_modelName . " record with $_property " . $_value . ' not found!');
133 $result = $resultSet->getFirstRecord();
136 throw new Tinebase_Exception_UnexpectedValue(' in total ' . count($resultSet) . ' where found. But only one should!');
143 * Calendar optimized search function
145 * 1. get all events neglecting grants filter
146 * 2. get all related container grants (via resolveing)
147 * 3. compute effective grants in PHP and only keep events
148 * user has required grant for
150 * @TODO rethink if an outer container filter could help
152 * @param Tinebase_Model_Filter_FilterGroup $_filter
153 * @param Tinebase_Model_Pagination $_pagination
154 * @param boolean $_onlyIds
155 * @return Tinebase_Record_RecordSet|array
157 public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_onlyIds = FALSE)
159 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Searching events ...');
161 if ($_pagination === NULL) {
162 $_pagination = new Tinebase_Model_Pagination();
165 $getDeleted = !!$_filter && $_filter->getFilter('is_deleted');
166 $select = parent::_getSelect('*', $getDeleted);
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')));
173 // NOTE: we join here as attendee and role filters need it
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());
181 /* table */ array('dispcontainer' => $this->_tablePrefix . 'container'),
182 /* on */ $this->_db->quoteIdentifier('dispcontainer.id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id'),
183 /* select */ array());
185 $select->where($this->_db->quoteIdentifier('dispcontainer.is_deleted') . ' = 0 OR ' . $this->_db->quoteIdentifier('dispcontainer.is_deleted') . 'IS NULL');
188 // remove grantsfilter here as we do grants computation in PHP
189 $grantsFilter = $_filter->getFilter('grants');
191 $_filter->removeFilter('grants');
194 // clonde the filter, as the filter is also used in the json frontend
195 // and the calendarfilter is used in the UI to
196 $clonedFilters = clone $_filter;
198 $calendarFilter = null;
199 foreach ($clonedFilters as $filter) {
200 if ($filter instanceof Calendar_Model_CalendarFilter) {
201 $calendarFilter = $filter;
202 $clonedFilters->removeFilter($filter);
207 $this->_addFilter($select, $clonedFilters);
209 $select->group($this->_tableName . '.' . 'id');
210 Tinebase_Backend_Sql_Abstract::traitGroup($select);
212 $period = $_filter->getFilter('period');
214 // filter out unnecessary YEARLY candidates for web client requests
215 // periods < 40 days should be client web client requests.
216 if ($period && $period->getFrom()->getClone()->addDay(40) > $period->getUntil()) {
217 $this->_removeNonMatchingBaseEvents($select, $period);
220 if ($calendarFilter) {
221 $select1 = clone $select;
222 $select2 = clone $select;
224 $calendarFilter->appendFilterSql1($select1, $this);
225 $calendarFilter->appendFilterSql2($select2, $this);
227 $select = $this->getAdapter()->select()->union(array(
233 $_pagination->appendPaginationSql($select);
235 $stmt = $this->_db->query($select);
236 $rows = (array)$stmt->fetchAll(Zend_Db::FETCH_ASSOC);
238 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
239 . ' Event base rows fetched: ' . count($rows) . ' select: ' . $select);
241 $result = $this->_rawDataToRecordSet($rows);
243 $this->_checkGrants($result, $grantsFilter);
245 return $_onlyIds ? $result->{is_bool($_onlyIds) ? $this->_getRecordIdentifier() : $_onlyIds} : $result;
249 * removes non-matching rrule base events
251 * @param Zend_Db_Select $select
252 * @param Calendar_Model_PeriodFilter $period
254 * TODO improve this by moving the rrules to a separate table to simplify SQL statment
256 protected function _removeNonMatchingBaseEvents($select, $period)
258 $gs = new Tinebase_Backend_Sql_Filter_GroupSelect($select);
259 $from = $period->getFrom()->getClone()->subDay(1);
260 $fromMonth = $from->format('n');
261 $fromDay = $from->format('j');
262 $until = $period->getUntil()->getClone()->addDay(1);
263 $untilMonth = $until->format('n');
264 $untilDay = $until->format('j');
265 $quotedRrule = $this->_db->quoteIdentifier('rrule');
267 $gs->where($quotedRrule . ' NOT LIKE ?', 'FREQ=YEARLY%')
268 ->orWhere($quotedRrule . ' IS NULL');
270 // TODO improve view detection to handle year boundaries, too
271 if ($fromMonth > $untilMonth) {
272 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
273 . ' Skipping performance optimization because we can\'t handle year boundaries yet');
276 if ($fromMonth == $untilMonth && $untilDay-$fromDay <= 10) {
278 for ($day=$fromDay; $day<=$untilDay; $day++) {
279 $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$fromMonth};BYMONTHDAY={$day}%");
283 for ($month=$fromMonth; $month<=$untilMonth; $month++) {
284 $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$month};%");
288 $gs->appendWhere(Zend_Db_Select::SQL_AND);
290 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
295 * calculate event permissions and remove events that don't match
297 * @param Tinebase_Record_RecordSet $events
298 * @param Tinebase_Model_Filter_AclFilter $grantsFilter
300 protected function _checkGrants($events, $grantsFilter)
302 $currentContact = Tinebase_Core::getUser()->contact_id;
303 $containerGrants = Tinebase_Container::getInstance()->getContainerGrantsOfRecords($events, Tinebase_Core::getUser());
304 $resolvedAttendees = Calendar_Model_Attender::getResolvedAttendees($events->attendee, true);
307 $inheritableGrants = array(
308 Tinebase_Model_Grants::GRANT_FREEBUSY,
309 Tinebase_Model_Grants::GRANT_READ,
310 Tinebase_Model_Grants::GRANT_SYNC,
311 Tinebase_Model_Grants::GRANT_EXPORT,
312 Tinebase_Model_Grants::GRANT_PRIVATE,
315 if ($grantsFilter instanceof Calendar_Model_GrantFilter) {
316 $requiredGrants = $grantsFilter->getRequiredGrants();
317 if (is_array($requiredGrants)) {
318 $requiredGrants = array_intersect($requiredGrants, $this->_recordBasedGrants);
320 // TODO throw exception here?
321 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
322 . ' Required grants not set in grants filter: ' . print_r($grantsFilter->toArray(), true));
326 foreach ($events as $event) {
327 $containerId = $event->container_id instanceof Tinebase_Model_Container
328 ? $event->container_id->getId()
329 : $event->container_id;
331 // either current user is organizer or has admin right on container
332 if ( $event->organizer === $currentContact
333 || (isset($containerGrants[$containerId]) && $containerGrants[$containerId]->account_grants[Tinebase_Model_Grants::GRANT_ADMIN])
335 foreach ($this->_recordBasedGrants as $grant) {
336 $event->{$grant} = true;
339 // has all rights => no need to filter
343 // grants to original container
344 if (isset($containerGrants[$containerId])) {
345 foreach ($this->_recordBasedGrants as $grant) {
346 $event->{$grant} = $containerGrants[$containerId]->account_grants[$grant];
350 // check grant inheritance
351 if ($event->attendee instanceof Tinebase_Record_RecordSet) {
352 foreach ($inheritableGrants as $grant) {
353 if (! $event->{$grant}) {
354 foreach ($event->attendee as $attendee) {
355 $attendee = $resolvedAttendees->getById($attendee->getId());
361 if ( $attendee->displaycontainer_id instanceof Tinebase_Model_Container
362 && $attendee->displaycontainer_id->account_grants
363 && ( $attendee->displaycontainer_id->account_grants[$grant]
364 || $attendee->displaycontainer_id->account_grants[Tinebase_Model_Grants::GRANT_ADMIN]
367 $event->{$grant} = true;
375 // check if one of the grants is set ...
376 if (isset($requiredGrants) && is_array($requiredGrants)) {
377 foreach ($requiredGrants as $requiredGrant) {
378 if ($event->{$requiredGrant}) {
383 // ... otherwise mark for removal
384 $toRemove[] = $event;
388 // remove records with non matching grants
389 foreach ($toRemove as $event) {
390 $events->removeRecord($event);
395 * get the basic select object to fetch records from the database
397 * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
398 * @param boolean $_getDeleted get deleted records (if modlog is active)
399 * @return Zend_Db_Select
401 protected function _getSelectSimple($_cols = '*', $_getDeleted = FALSE)
403 $select = $this->_db->select();
405 $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $_cols);
407 if (!$_getDeleted && $this->_modlogActive) {
408 // don't fetch deleted objects
409 $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
416 * Gets total count of search with $_filter
418 * @param Tinebase_Model_Filter_FilterGroup $_filter
421 public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
423 $select = $this->_getSelect(array('count' => 'COUNT(*)'));
424 $this->_addFilter($select, $_filter);
426 $result = $this->_db->fetchOne($select);
432 * Updates existing entry
434 * @param Tinebase_Record_Interface $_record
435 * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
436 * @return Tinebase_Record_Interface Record|NULL
438 public function update(Tinebase_Record_Interface $_record)
440 if ($_record->rrule) {
441 $_record->rrule = (string) $_record->rrule;
444 if ($_record->container_id instanceof Tinebase_Model_Container) {
445 $_record->container_id = $_record->container_id->getId();
448 $_record->rrule = !empty($_record->rrule) ? $_record->rrule : NULL;
449 $_record->recurid = !empty($_record->recurid) ? $_record->recurid : NULL;
451 $event = parent::update($_record);
452 $this->_saveExdates($_record);
454 return $this->get($event->getId(), TRUE);
458 * get the basic select object to fetch records from the database
460 * @param array|string|Zend_Db_Expr $_cols columns to get, * per default
461 * @param boolean $_getDeleted get deleted records (if modlog is active)
462 * @return Zend_Db_Select
464 protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
466 $select = $this->_getSelectSimple();
468 $this->_appendEffectiveGrantCalculationSql($select);
471 /* table */ array('exdate' => $this->_tablePrefix . 'cal_exdate'),
472 /* on */ $this->_db->quoteIdentifier('exdate.cal_event_id') . ' = ' . $this->_db->quoteIdentifier($this->_tableName . '.id'),
473 /* select */ array('exdate' => $this->_dbCommand->getAggregate('exdate.exdate')));
475 $select->group($this->_tableName . '.' . 'id');
481 * appends effective grant calculation to select object
483 * @param Zend_Db_Select $_select
485 protected function _appendEffectiveGrantCalculationSql($_select, $_attendeeFilters = NULL)
487 // groupmemberships of current user, needed to compute phys and inherited grants
489 /* table */ array('groupmemberships' => $this->_tablePrefix . 'group_members'),
490 /* on */ $this->_db->quoteInto($this->_db->quoteIdentifier('groupmemberships.account_id') . ' = ?' , Tinebase_Core::getUser()->getId()),
491 /* select */ array());
493 // attendee joins the attendee we need to compute the curr users effective grants
494 // NOTE: 2010-04 the behaviour changed. Now, only the attendee the client filters for are
495 // taken into account for grants calculation
496 $attendeeWhere = FALSE;
497 if (is_array($_attendeeFilters) && !empty($_attendeeFilters)) {
498 $attendeeSelect = $this->_db->select();
499 foreach ((array) $_attendeeFilters as $attendeeFilter) {
500 if ($attendeeFilter instanceof Calendar_Model_AttenderFilter) {
501 $attendeeFilter->appendFilterSql($attendeeSelect, $this);
505 $whereArray = $attendeeSelect->getPart(Zend_Db_Select::SQL_WHERE);
506 if (! empty($whereArray)) {
507 $attendeeWhere = ' AND ' . array_value(0, $whereArray);
512 /* table */ array('attendee' => $this->_tablePrefix . 'cal_attendee'),
513 /* on */ $this->_db->quoteIdentifier('attendee.cal_event_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.id') .
515 /* select */ array());
520 /* table */ array('attendeeaccounts' => $this->_tablePrefix . 'accounts'),
521 /* on */ $this->_db->quoteIdentifier('attendeeaccounts.contact_id') . ' = ' . $this->_db->quoteIdentifier('attendee.user_id') . ' AND (' .
522 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
523 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . '= ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) .
525 /* select */ array());
528 /* table */ array('attendeegroupmemberships' => $this->_tablePrefix . 'group_members'),
529 /* on */ $this->_db->quoteIdentifier('attendeegroupmemberships.account_id') . ' = ' . $this->_db->quoteIdentifier('attendeeaccounts.contact_id'),
530 /* select */ array());
535 /* table */ array('dispgrants' => $this->_tablePrefix . 'container_acl'),
536 /* on */ $this->_db->quoteIdentifier('dispgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('attendee.displaycontainer_id') .
537 ' AND ' . $this->_getContainGrantCondition('dispgrants', 'groupmemberships'),
538 /* select */ array());
541 /* table */ array('physgrants' => $this->_tablePrefix . 'container_acl'),
542 /* on */ $this->_db->quoteIdentifier('physgrants.container_id') . ' = ' . $this->_db->quoteIdentifier('cal_events.container_id'),
543 /* select */ array());
545 $allGrants = Tinebase_Model_Grants::getAllGrants();
547 foreach ($allGrants as $grant) {
548 if (in_array($grant, $this->_recordBasedGrants)) {
549 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
550 ' /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . " OR \n" .
551 ' /* implicit */' . $this->_getImplicitGrantCondition($grant) . " OR \n" .
552 ' /* inherited */' . $this->_getInheritedGrantCondition($grant) . " \n" .
553 ") THEN 1 ELSE 0 END ) "));
555 $_select->columns(array($grant => "\n MAX( CASE WHEN ( \n" .
556 ' /* physgrant */' . $this->_getContainGrantCondition('physgrants', 'groupmemberships', $grant) . "\n" .
557 ") THEN 1 ELSE 0 END ) "));
563 * returns SQL with container grant condition
565 * @param string $_aclTableName
566 * @param string $_groupMembersTableName
567 * @param string|array $_requiredGrant (defaults none)
568 * @param Zend_Db_Expr|int|Tinebase_Model_User $_user (defaults current user)
571 protected function _getContainGrantCondition($_aclTableName, $_groupMembersTableName, $_requiredGrant=NULL, $_user=NULL )
573 $quoteTypeIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_type');
574 $quoteIdIdentifier = $this->_db->quoteIdentifier($_aclTableName . '.account_id');
576 if ($_user instanceof Zend_Db_Expr) {
577 $userExpression = $_user;
579 $accountId = $_user ? Tinebase_Model_User::convertUserIdToInt($_user) : Tinebase_Core::getUser()->getId();
580 $userExpression = new Zend_Db_Expr($this->_db->quote($accountId));
583 $sql = $this->_db->quoteInto( "($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_USER) . " AND $quoteIdIdentifier = $userExpression)" .
584 $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP) . ' AND ' . $this->_db->quoteIdentifier("$_groupMembersTableName.group_id") . " = $quoteIdIdentifier" . ')' .
585 $this->_db->quoteInto(" OR ($quoteTypeIdentifier = ?)", Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE);
587 if ($_requiredGrant) {
588 $sql = "($sql) AND " . $this->_db->quoteInto($this->_db->quoteIdentifier($_aclTableName . '.account_grant') . ' IN (?)', (array)$_requiredGrant);
596 * returns SQL condition for implicit grants
598 * @param string $_requiredGrant
599 * @param Tinebase_Model_User $_user (defaults to current user)
602 protected function _getImplicitGrantCondition($_requiredGrant, $_user=NULL)
604 $accountId = $_user ? $_user->getId() : Tinebase_Core::getUser()->getId();
605 $contactId = $_user ? $_user->contact_id : Tinebase_Core::getUser()->contact_id;
607 // delte grant couldn't be gained implicitly
608 if ($_requiredGrant == Tinebase_Model_Grants::GRANT_DELETE) {
612 // organizer gets all other grants implicitly
613 $sql = $this->_db->quoteIdentifier('cal_events.organizer') . " = " . $this->_db->quote($contactId);
615 // attendee get read, sync, export and private grants implicitly
616 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))) {
617 $readCond = $this->_db->quoteIdentifier('attendeeaccounts.id') . ' = ' . $this->_db->quote($accountId) . ' AND (' .
618 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_USER) . ' OR ' .
619 $this->_db->quoteInto($this->_db->quoteIdentifier('attendee.user_type') . ' = ?', Calendar_Model_Attender::USERTYPE_GROUPMEMBER) .
622 $sql = "($sql) OR ($readCond)";
629 * returns SQL for inherited grants
631 * @param string $_requiredGrant
634 protected function _getInheritedGrantCondition($_requiredGrant)
636 // current user needs to have grant on display calendar
637 $sql = $this->_getContainGrantCondition('dispgrants', 'groupmemberships', $_requiredGrant);
639 // _AND_ attender(admin) of display calendar needs to have grant on phys calendar
640 // @todo include implicit inherited grants
641 if (! in_array($_requiredGrant, array(Tinebase_Model_Grants::GRANT_READ, Tinebase_Model_Grants::GRANT_FREEBUSY))) {
642 $userExpr = new Zend_Db_Expr($this->_db->quoteIdentifier('attendeeaccounts.id'));
644 $attenderPhysGrantCond = $this->_getContainGrantCondition('physgrants', 'attendeegroupmemberships', $_requiredGrant, $userExpr);
645 // NOTE: this condition is weak! Not some attendee must have implicit grant.
646 // -> an attender we have reqired grants for his diplay cal must have implicit grants
647 //$attenderImplicitGrantCond = $this->_getImplicitGrantCondition($_requiredGrant, $userExpr);
649 //$sql = "($sql) AND ($attenderPhysGrantCond) OR ($attenderImplicitGrantCond)";
650 $sql = "($sql) AND ($attenderPhysGrantCond)";
657 * converts raw data from adapter into a single record
659 * @param array $_data
660 * @return Tinebase_Record_Abstract
662 protected function _rawDataToRecord(array $_rawData) {
663 $event = parent::_rawDataToRecord($_rawData);
665 $this->appendForeignRecordSetToRecord($event, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
671 * converts raw data from adapter into a set of records
673 * @param array $_rawData of arrays
674 * @return Tinebase_Record_RecordSet
676 protected function _rawDataToRecordSet(array $_rawData)
678 $events = new Tinebase_Record_RecordSet($this->_modelName);
679 $events->addIndices(array('rrule', 'recurid'));
681 foreach ($_rawData as $rawEvent) {
682 $events->addRecord(new Calendar_Model_Event($rawEvent, true));
685 $this->appendForeignRecordSetToRecordSet($events, 'attendee', 'id', Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT, $this->_attendeeBackend);
691 * saves exdates of an event
693 * @param Calendar_Model_Event $_event
695 protected function _saveExdates($_event)
697 $this->_db->delete($this->_tablePrefix . 'cal_exdate', $this->_db->quoteInto($this->_db->quoteIdentifier('cal_event_id') . '= ?', $_event->getId()));
699 // only save exdates if its an recurring event
700 if (! empty($_event->rrule)) {
701 foreach ((array)$_event->exdate as $exdate) {
702 if (is_object($exdate)) {
703 $this->_db->insert($this->_tablePrefix . 'cal_exdate', array(
704 'id' => $_event->generateUID(),
705 'cal_event_id' => $_event->getId(),
706 'exdate' => $exdate->get(Tinebase_Record_Abstract::ISO8601LONG)
709 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
710 . ' Exdate needs to be an object:' . var_export($exdate, TRUE));
717 * saves attendee of given event
719 * @param Calendar_Model_Evnet $_event
721 protected function _saveAttendee($_event)
723 $attendee = $_event->attendee instanceof Tinebase_Record_RecordSet ?
725 new Tinebase_Record_RecordSet($this->_attendeeBackend->getModelName());
726 $attendee->cal_event_id = $_event->getId();
728 $currentAttendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
730 $diff = $currentAttendee->getMigration($attendee->getArrayOfIds());
731 $this->_attendeeBackend->delete($diff['toDeleteIds']);
733 foreach ($attendee as $attende) {
734 $method = $attende->getId() ? 'update' : 'create';
735 $this->_attendeeBackend->$method($attende);
740 /****************************** attendee functions ************************/
743 * gets attendee of a given event
745 * @param Calendar_Model_Event $_event
746 * @return Tinebase_Record_RecordSet
748 public function getEventAttendee(Calendar_Model_Event $_event)
750 $attendee = $this->_attendeeBackend->getMultipleByProperty($_event->getId(), Calendar_Backend_Sql_Attendee::FOREIGNKEY_EVENT);
756 * creates given attender in database
758 * @param Calendar_Model_Attender $_attendee
759 * @return Calendar_Model_Attender
761 public function createAttendee(Calendar_Model_Attender $_attendee)
763 if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
764 $_attendee->user_id = $_attendee->user_id->getId();
765 } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
766 $_attendee->user_id = $_attendee->user_id->group_id;
769 if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
770 $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
773 return $this->_attendeeBackend->create($_attendee);
777 * updates given attender in database
779 * @param Calendar_Model_Attender $_attendee
780 * @return Calendar_Model_Attender
782 public function updateAttendee(Calendar_Model_Attender $_attendee)
784 if ($_attendee->user_id instanceof Addressbook_Model_Contact) {
785 $_attendee->user_id = $_attendee->user_id->getId();
786 } else if ($_attendee->user_id instanceof Addressbook_Model_List) {
787 $_attendee->user_id = $_attendee->user_id->group_id;
790 if ($_attendee->displaycontainer_id instanceof Tinebase_Model_Container) {
791 $_attendee->displaycontainer_id = $_attendee->displaycontainer_id->getId();
794 return $this->_attendeeBackend->update($_attendee);
798 * deletes given attender in database
800 * @param Calendar_Model_Attender $_attendee
803 public function deleteAttendee(array $_ids)
805 return $this->_attendeeBackend->delete($_ids);
809 * delete duplicate events defined by an event filter
811 * @param Calendar_Model_EventFilter $filter
812 * @param boolean $dryrun
813 * @return integer number of deleted events
815 public function deleteDuplicateEvents($filter, $dryrun = TRUE)
817 if ($dryrun && Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
818 . ' - Running in dry run mode - using filter: ' . print_r($filter->toArray(), true));
820 $duplicateFields = array('summary', 'dtstart', 'dtend');
822 $select = $this->_db->select();
823 $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $duplicateFields);
824 $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
826 $this->_addFilter($select, $filter);
828 $select->group($duplicateFields)
829 ->having('count(*) > 1');
831 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
834 $rows = $this->_fetch($select, self::FETCH_ALL);
836 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
837 . ' ' . print_r($rows, TRUE));
840 foreach ($rows as $row) {
841 $index = $row['summary'] . ' / ' . $row['dtstart'] . ' - ' . $row['dtend'];
843 $filter = new Calendar_Model_EventFilter(array(array(
844 'field' => 'summary',
845 'operator' => 'equals',
846 'value' => $row['summary'],
848 'field' => 'dtstart',
849 'operator' => 'equals',
850 'value' => new Tinebase_DateTime($row['dtstart']),
853 'operator' => 'equals',
854 'value' => new Tinebase_DateTime($row['dtend']),
856 $pagination = new Tinebase_Model_Pagination(array('sort' => array($this->_tableName . '.last_modified_time', $this->_tableName . '.creation_time')));
858 $select = $this->_db->select();
859 $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName));
860 $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
862 $this->_addFilter($select, $filter);
863 $pagination->appendPaginationSql($select);
865 $rows = $this->_fetch($select, self::FETCH_ALL);
866 $events = $this->_rawDataToRecordSet($rows);
868 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
869 . ' ' . print_r($events->toArray(), TRUE));
871 $deleteIds = $events->getArrayOfIds();
873 array_shift($deleteIds);
875 if (! empty($deleteIds)) {
876 $deleteContainerIds = ($events->container_id);
877 $origContainer = array_shift($deleteContainerIds);
878 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
879 . ' Deleting ' . count($deleteIds) . ' duplicates of: ' . $index . ' in container_ids ' . implode(',', $deleteContainerIds) . ' (origin container: ' . $origContainer . ')');
880 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
881 . ' ' . print_r($deleteIds, TRUE));
883 $toDelete = array_merge($toDelete, $deleteIds);
885 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
886 . ' No duplicates found for ' . $index);
890 if (empty($toDelete)) {
891 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
892 . ' No duplicates found.');
895 $result = ($dryrun) ? count($toDelete) : $this->delete($toDelete);
902 * repair dangling attendee records (no displaycontainer_id)
904 * @see https://forge.tine20.org/mantisbt/view.php?id=8172
906 public function repairDanglingDisplaycontainerEvents()
908 $filter = new Tinebase_Model_Filter_FilterGroup();
909 $filter->addFilter(new Tinebase_Model_Filter_Text(array(
910 'field' => 'user_type',
913 Calendar_Model_Attender::USERTYPE_USER,
914 Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
915 Calendar_Model_Attender::USERTYPE_RESOURCE
919 $filter->addFilter(new Tinebase_Model_Filter_Text(array(
920 'field' => 'displaycontainer_id',
921 'operator' => 'isnull',
925 $danglingAttendee = $this->_attendeeBackend->search($filter);
926 $danglingContactAttendee = $danglingAttendee->filter('user_type', '/'. Calendar_Model_Attender::USERTYPE_USER . '|'. Calendar_Model_Attender::USERTYPE_GROUPMEMBER .'/', TRUE);
927 $danglingContactIds = array_unique($danglingContactAttendee->user_id);
928 $danglingContacts = Addressbook_Controller_Contact::getInstance()->getMultiple($danglingContactIds, TRUE);
929 $danglingResourceAttendee = $danglingAttendee->filter('user_type', Calendar_Model_Attender::USERTYPE_RESOURCE);
930 $danglingResourceIds = array_unique($danglingResourceAttendee->user_id);
931 Calendar_Controller_Resource::getInstance()->doContainerACLChecks(false);
932 $danglingResources = Calendar_Controller_Resource::getInstance()->getMultiple($danglingResourceIds, TRUE);
934 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
935 . ' Processing ' . count($danglingContactIds) . ' dangling contact ids...');
937 foreach ($danglingContactIds as $danglingContactId) {
938 $danglingContact = $danglingContacts->getById($danglingContactId);
939 if ($danglingContact && $danglingContact->account_id) {
941 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
942 . ' Get default display container for account ' . $danglingContact->account_id);
943 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
944 . ' ' . print_r($danglingContact->toArray(), true));
946 $displayCalId = Calendar_Controller_Event::getDefaultDisplayContainerId($danglingContact->account_id);
948 // finaly repair attendee records
949 $attendeeRecords = $danglingContactAttendee->filter('user_id', $danglingContactId);
950 $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
951 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following contact attendee " . print_r($attendeeRecords->toArray(), TRUE));
956 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
957 . ' Processing ' . count($danglingResourceIds) . ' dangling resource ids...');
959 foreach ($danglingResourceIds as $danglingResourceId) {
960 $resource = $danglingResources->getById($danglingResourceId);
961 if ($resource && $resource->container_id) {
962 $displayCalId = $resource->container_id;
963 $attendeeRecords = $danglingResourceAttendee->filter('user_id', $danglingResourceId);
964 $this->_attendeeBackend->updateMultiple($attendeeRecords->getId(), array('displaycontainer_id' => $displayCalId));
965 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " repaired the following resource attendee " . print_r($attendeeRecords->toArray(), TRUE));