Take differing parstat values in recurrence exceptions into account when querying for pending/declined/regular events:

- Colelct partstat tags from recurrence exceptions when caching
- Querying for 'tags != x-partstat:<email>:needs-action' may miss some valid records
- Do post-filtering on all events, including recurring instances
This commit is contained in:
Thomas Bruederli 2015-02-16 11:00:26 +01:00
parent 8a74dc2d28
commit aaaa9c5818
5 changed files with 70 additions and 61 deletions

View file

@ -450,6 +450,7 @@ abstract class calendar_driver
$rcmail = rcmail::get_instance();
$recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
// determine a reasonable end date if none given
if (!$end) {
@ -465,10 +466,10 @@ abstract class calendar_driver
$i = 0;
while ($next_event = $recurrence->next_instance()) {
$next_event['uid'] = $event['uid'] . '-' . ++$i;
// add to output if in range
if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
$next_event['id'] = $next_event['uid'];
$next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
$next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
$next_event['recurrence_id'] = $event['uid'];
$events[] = $next_event;
}
@ -477,7 +478,7 @@ abstract class calendar_driver
}
// avoid endless recursion loops
if ($i > 1000) {
if (++$i > 1000) {
break;
}
}

View file

@ -236,35 +236,31 @@ class kolab_calendar extends kolab_storage_folder_api
$query[] = array('dtstart', '<=', $end);
$query[] = array('dtend', '>=', $start);
// add query to exclude pending/declined invitations
if (empty($filter_query) && $this->get_namespace() != 'other') {
foreach ($user_emails as $email) {
$query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
$query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
}
}
else if (is_array($filter_query)) {
if (is_array($filter_query)) {
$query = array_merge($query, $filter_query);
}
if (!empty($search)) {
$search = mb_strtolower($search);
$words = rcube_utils::tokenize_string($search, 1);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', 'LIKE', $word);
}
}
else {
$words = array();
}
// set partstat filter to skip pending and declined invitations
if (empty($filter_query) && $this->get_namespace() != 'other') {
$partstat_exclude = array('NEEDS-ACTION','DECLINED');
}
else {
$partstat_exclude = array();
}
$events = array();
foreach ($this->storage->select($query) as $record) {
// post-filter events to skip pending and declined invitations
if (empty($filter_query) && is_array($record['attendees']) && $this->get_namespace() != 'other') {
foreach ($record['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], array('NEEDS-ACTION','DECLINED'))) {
continue 2;
}
}
}
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
@ -272,18 +268,6 @@ class kolab_calendar extends kolab_storage_folder_api
if ($event['categories'])
$this->categories[$event['categories']]++;
// filter events by search query
if (!empty($search)) {
$hits = 0;
$words = rcube_utils::tokenize_string($search, 1);
foreach ($words as $word) {
$hits += $this->_fulltext_match($event, $word);
}
if ($hits < count($words)) // skip this event if not match with search term
continue;
}
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
@ -312,25 +296,39 @@ class kolab_calendar extends kolab_storage_folder_api
if ($add)
$events[] = $event;
}
// resolve recurring events
if ($record['recurrence'] && $virtual == 1) {
$events = array_merge($events, $this->get_recurring_events($record, $start, $end));
// when searching, only recurrence exceptions may match the query so post-filter the results again
if (!empty($search) && $record['recurrence']['EXCEPTIONS']) {
$me = $this;
$events = array_filter($events, function($event) use ($words, $me) {
$hits = 0;
foreach ($words as $word) {
$hits += $me->_fulltext_match($event, $word, false);
}
return $hits >= count($words);
});
}
}
}
// post-filter all events by fulltext search and partstat values
$me = $this;
$events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
// fulltext search
if (count($words)) {
$hits = 0;
foreach ($words as $word) {
$hits += $me->_fulltext_match($event, $word, false);
}
if ($hits < count($words)) {
return false;
}
}
// partstat filter
if (count($partstat_exclude) && is_array($event['attendees'])) {
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
return false;
}
}
}
return true;
});
// avoid session race conditions that will loose temporary subscriptions
$this->cal->rc->session->nowrite = true;

View file

@ -223,15 +223,16 @@ class kolab_format_event extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
public function get_tags()
public function get_tags($obj = null)
{
$tags = parent::get_tags();
$tags = parent::get_tags($obj);
$object = $obj ?: $this->data;
foreach ((array)$this->data['categories'] as $cat) {
foreach ((array)$object['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
return $tags;
return array_unique($tags);
}
/**

View file

@ -121,20 +121,21 @@ class kolab_format_task extends kolab_format_xcal
*
* @return array List of tags to save in cache
*/
public function get_tags()
public function get_tags($obj = null)
{
$tags = parent::get_tags();
$tags = parent::get_tags($obj);
$object = $obj ?: $this->data;
if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
if ($object['status'] == 'COMPLETED' || ($object['complete'] == 100 && empty($object['status'])))
$tags[] = 'x-complete';
if ($this->data['priority'] == 1)
if ($object['priority'] == 1)
$tags[] = 'x-flagged';
if ($this->data['parent_id'])
$tags[] = 'x-parent:' . $this->data['parent_id'];
if ($object['parent_id'])
$tags[] = 'x-parent:' . $object['parent_id'];
return $tags;
return array_unique($tags);
}
}

View file

@ -611,23 +611,31 @@ abstract class kolab_format_xcal extends kolab_format
*
* @return array List of tags to save in cache
*/
public function get_tags()
public function get_tags($obj = null)
{
$tags = array();
$object = $obj ?: $this->data;
if (!empty($this->data['valarms'])) {
if (!empty($object['valarms'])) {
$tags[] = 'x-has-alarms';
}
// create tags reflecting participant status
if (is_array($this->data['attendees'])) {
foreach ($this->data['attendees'] as $attendee) {
if (is_array($object['attendees'])) {
foreach ($object['attendees'] as $attendee) {
if (!empty($attendee['email']) && !empty($attendee['status']))
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
}
}
return $tags;
// collect tags from recurrence exceptions
if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$tags = array_merge($tags, $this->get_tags($exception));
}
}
return array_unique($tags);
}
/**