comparison plugins/calendar/drivers/kolab/kolab_calendar.php @ 3:f6fe4b6ae66a

calendar plugin nearly as distributed
author Charlie Root
date Sat, 13 Jan 2018 08:56:12 -0500
parents
children
comparison
equal deleted inserted replaced
2:c828b0fd4a6e 3:f6fe4b6ae66a
1 <?php
2
3 /**
4 * Kolab calendar storage class
5 *
6 * @version @package_version@
7 * @author Thomas Bruederli <bruederli@kolabsys.com>
8 * @author Aleksander Machniak <machniak@kolabsys.com>
9 *
10 * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
11 *
12 * This program is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU Affero General Public License as
14 * published by the Free Software Foundation, either version 3 of the
15 * License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU Affero General Public License for more details.
21 *
22 * You should have received a copy of the GNU Affero General Public License
23 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 */
25
26
27 class kolab_calendar extends kolab_storage_folder_api
28 {
29 public $ready = false;
30 public $rights = 'lrs';
31 public $editable = false;
32 public $attachments = true;
33 public $alarms = false;
34 public $history = false;
35 public $subscriptions = true;
36 public $categories = array();
37 public $storage;
38
39 public $type = 'event';
40
41 protected $cal;
42 protected $events = array();
43 protected $search_fields = array('title', 'description', 'location', 'attendees');
44
45 /**
46 * Factory method to instantiate a kolab_calendar object
47 *
48 * @param string Calendar ID (encoded IMAP folder name)
49 * @param object calendar plugin object
50 * @return object kolab_calendar instance
51 */
52 public static function factory($id, $calendar)
53 {
54 $imap = $calendar->rc->get_storage();
55 $imap_folder = kolab_storage::id_decode($id);
56 $info = $imap->folder_info($imap_folder, true);
57 if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
58 return new kolab_user_calendar($imap_folder, $calendar);
59 }
60 else {
61 return new kolab_calendar($imap_folder, $calendar);
62 }
63 }
64
65 /**
66 * Default constructor
67 */
68 public function __construct($imap_folder, $calendar)
69 {
70 $this->cal = $calendar;
71 $this->imap = $calendar->rc->get_storage();
72 $this->name = $imap_folder;
73
74 // ID is derrived from folder name
75 $this->id = kolab_storage::folder_id($this->name, true);
76 $old_id = kolab_storage::folder_id($this->name, false);
77
78 // fetch objects from the given IMAP folder
79 $this->storage = kolab_storage::get_folder($this->name);
80 $this->ready = $this->storage && $this->storage->valid;
81
82 // Set writeable and alarms flags according to folder permissions
83 if ($this->ready) {
84 if ($this->storage->get_namespace() == 'personal') {
85 $this->editable = true;
86 $this->rights = 'lrswikxteav';
87 $this->alarms = true;
88 }
89 else {
90 $rights = $this->storage->get_myrights();
91 if ($rights && !PEAR::isError($rights)) {
92 $this->rights = $rights;
93 if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
94 $this->editable = strpos($rights, 'i');;
95 }
96 }
97
98 // user-specific alarms settings win
99 $prefs = $this->cal->rc->config->get('kolab_calendars', array());
100 if (isset($prefs[$this->id]['showalarms']))
101 $this->alarms = $prefs[$this->id]['showalarms'];
102 else if (isset($prefs[$old_id]['showalarms']))
103 $this->alarms = $prefs[$old_id]['showalarms'];
104 }
105
106 $this->default = $this->storage->default;
107 $this->subtype = $this->storage->subtype;
108 }
109
110
111 /**
112 * Getter for the IMAP folder name
113 *
114 * @return string Name of the IMAP folder
115 */
116 public function get_realname()
117 {
118 return $this->name;
119 }
120
121 /**
122 *
123 */
124 public function get_title()
125 {
126 return null;
127 }
128
129
130 /**
131 * Return color to display this calendar
132 */
133 public function get_color($default = null)
134 {
135 // color is defined in folder METADATA
136 if ($color = $this->storage->get_color()) {
137 return $color;
138 }
139
140 // calendar color is stored in user prefs (temporary solution)
141 $prefs = $this->cal->rc->config->get('kolab_calendars', array());
142
143 if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
144 return $prefs[$this->id]['color'];
145
146 return $default ?: 'cc0000';
147 }
148
149 /**
150 * Compose an URL for CalDAV access to this calendar (if configured)
151 */
152 public function get_caldav_url()
153 {
154 if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
155 return strtr($template, array(
156 '%h' => $_SERVER['HTTP_HOST'],
157 '%u' => urlencode($this->cal->rc->get_user_name()),
158 '%i' => urlencode($this->storage->get_uid()),
159 '%n' => urlencode($this->name),
160 ));
161 }
162
163 return false;
164 }
165
166
167 /**
168 * Update properties of this calendar folder
169 *
170 * @see calendar_driver::edit_calendar()
171 */
172 public function update(&$prop)
173 {
174 $prop['oldname'] = $this->get_realname();
175 $newfolder = kolab_storage::folder_update($prop);
176
177 if ($newfolder === false) {
178 $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
179 return false;
180 }
181
182 // create ID
183 return kolab_storage::folder_id($newfolder);
184 }
185
186 /**
187 * Getter for a single event object
188 */
189 public function get_event($id)
190 {
191 // remove our occurrence identifier if it's there
192 $master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
193
194 // directly access storage object
195 if (!$this->events[$id] && $master_id == $id && ($record = $this->storage->get_object($id))) {
196 $this->events[$id] = $this->_to_driver_event($record, true);
197 }
198
199 // maybe a recurring instance is requested
200 if (!$this->events[$id] && $master_id != $id) {
201 $instance_id = substr($id, strlen($master_id) + 1);
202
203 if ($record = $this->storage->get_object($master_id)) {
204 $master = $this->_to_driver_event($record);
205 }
206
207 if ($master) {
208 // check for match in top-level exceptions (aka loose single occurrences)
209 if ($master['_formatobj'] && ($instance = $master['_formatobj']->get_instance($instance_id))) {
210 $this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
211 }
212 // check for match on the first instance already
213 else if ($master['_instance'] && $master['_instance'] == $instance_id) {
214 $this->events[$id] = $master;
215 }
216 else if (is_array($master['recurrence'])) {
217 $this->get_recurring_events($record, $master['start'], null, $id);
218 }
219 }
220 }
221
222 return $this->events[$id];
223 }
224
225 /**
226 * Get attachment body
227 * @see calendar_driver::get_attachment_body()
228 */
229 public function get_attachment_body($id, $event)
230 {
231 if (!$this->ready)
232 return false;
233
234 $data = $this->storage->get_attachment($event['id'], $id);
235
236 if ($data == null) {
237 // try again with master UID
238 $uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
239 if ($uid != $event['id']) {
240 $data = $this->storage->get_attachment($uid, $id);
241 }
242 }
243
244 return $data;
245 }
246
247 /**
248 * @param integer Event's new start (unix timestamp)
249 * @param integer Event's new end (unix timestamp)
250 * @param string Search query (optional)
251 * @param boolean Include virtual events (optional)
252 * @param array Additional parameters to query storage
253 * @param array Additional query to filter events
254 * @return array A list of event records
255 */
256 public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
257 {
258 // convert to DateTime for comparisons
259 // #5190: make the range a little bit wider
260 // to workaround possible timezone differences
261 try {
262 $start = new DateTime('@' . ($start - 12 * 3600));
263 }
264 catch (Exception $e) {
265 $start = new DateTime('@0');
266 }
267 try {
268 $end = new DateTime('@' . ($end + 12 * 3600));
269 }
270 catch (Exception $e) {
271 $end = new DateTime('today +10 years');
272 }
273
274 // get email addresses of the current user
275 $user_emails = $this->cal->get_user_emails();
276
277 // query Kolab storage
278 $query[] = array('dtstart', '<=', $end);
279 $query[] = array('dtend', '>=', $start);
280
281 if (is_array($filter_query)) {
282 $query = array_merge($query, $filter_query);
283 }
284
285 if (!empty($search)) {
286 $search = mb_strtolower($search);
287 $words = rcube_utils::tokenize_string($search, 1);
288 foreach (rcube_utils::normalize_string($search, true) as $word) {
289 $query[] = array('words', 'LIKE', $word);
290 }
291 }
292 else {
293 $words = array();
294 }
295
296 // set partstat filter to skip pending and declined invitations
297 if (empty($filter_query) && $this->cal->rc->config->get('kolab_invitation_calendars')
298 && $this->get_namespace() != 'other'
299 ) {
300 $partstat_exclude = array('NEEDS-ACTION','DECLINED');
301 }
302 else {
303 $partstat_exclude = array();
304 }
305
306 $events = array();
307 foreach ($this->storage->select($query) as $record) {
308 $event = $this->_to_driver_event($record, !$virtual, false);
309
310 // remember seen categories
311 if ($event['categories']) {
312 $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
313 $this->categories[$cat]++;
314 }
315
316 // list events in requested time window
317 if ($event['start'] <= $end && $event['end'] >= $start) {
318 unset($event['_attendees']);
319 $add = true;
320
321 // skip the first instance of a recurring event if listed in exdate
322 if ($virtual && !empty($event['recurrence']['EXDATE'])) {
323 $event_date = $event['start']->format('Ymd');
324 $exdates = (array)$event['recurrence']['EXDATE'];
325
326 foreach ($exdates as $exdate) {
327 if ($exdate->format('Ymd') == $event_date) {
328 $add = false;
329 break;
330 }
331 }
332 }
333
334 // find and merge exception for the first instance
335 if ($virtual && !empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
336 foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
337 if ($event['_instance'] == $exception['_instance']) {
338 // clone date objects from main event before adjusting them with exception data
339 if (is_object($event['start'])) $event['start'] = clone $record['start'];
340 if (is_object($event['end'])) $event['end'] = clone $record['end'];
341 kolab_driver::merge_exception_data($event, $exception);
342 }
343 }
344 }
345
346 if ($add)
347 $events[] = $event;
348 }
349
350 // resolve recurring events
351 if ($record['recurrence'] && $virtual == 1) {
352 $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
353 }
354 // add top-level exceptions (aka loose single occurrences)
355 else if (is_array($record['exceptions'])) {
356 foreach ($record['exceptions'] as $ex) {
357 $component = $this->_to_driver_event($ex, false, false, $record);
358 if ($component['start'] <= $end && $component['end'] >= $start) {
359 $events[] = $component;
360 }
361 }
362 }
363 }
364
365 // post-filter all events by fulltext search and partstat values
366 $me = $this;
367 $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
368 // fulltext search
369 if (count($words)) {
370 $hits = 0;
371 foreach ($words as $word) {
372 $hits += $me->fulltext_match($event, $word, false);
373 }
374 if ($hits < count($words)) {
375 return false;
376 }
377 }
378
379 // partstat filter
380 if (count($partstat_exclude) && is_array($event['attendees'])) {
381 foreach ($event['attendees'] as $attendee) {
382 if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
383 return false;
384 }
385 }
386 }
387
388 return true;
389 });
390
391 // Apply event-to-mail relations
392 $config = kolab_storage_config::get_instance();
393 $config->apply_links($events);
394
395 // avoid session race conditions that will loose temporary subscriptions
396 $this->cal->rc->session->nowrite = true;
397
398 return $events;
399 }
400
401 /**
402 *
403 * @param integer Date range start (unix timestamp)
404 * @param integer Date range end (unix timestamp)
405 * @param array Additional query to filter events
406 * @return integer Count
407 */
408 public function count_events($start, $end = null, $filter_query = null)
409 {
410 // convert to DateTime for comparisons
411 try {
412 $start = new DateTime('@'.$start);
413 }
414 catch (Exception $e) {
415 $start = new DateTime('@0');
416 }
417 if ($end) {
418 try {
419 $end = new DateTime('@'.$end);
420 }
421 catch (Exception $e) {
422 $end = null;
423 }
424 }
425
426 // query Kolab storage
427 $query[] = array('dtend', '>=', $start);
428
429 if ($end)
430 $query[] = array('dtstart', '<=', $end);
431
432 // add query to exclude pending/declined invitations
433 if (empty($filter_query)) {
434 foreach ($this->cal->get_user_emails() as $email) {
435 $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
436 $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
437 }
438 }
439 else if (is_array($filter_query)) {
440 $query = array_merge($query, $filter_query);
441 }
442
443 // we rely the Kolab storage query (no post-filtering)
444 return $this->storage->count($query);
445 }
446
447 /**
448 * Create a new event record
449 *
450 * @see calendar_driver::new_event()
451 *
452 * @return mixed The created record ID on success, False on error
453 */
454 public function insert_event($event)
455 {
456 if (!is_array($event))
457 return false;
458
459 // email links are stored separately
460 $links = $event['links'];
461 unset($event['links']);
462
463 //generate new event from RC input
464 $object = $this->_from_driver_event($event);
465 $saved = $this->storage->save($object, 'event');
466
467 if (!$saved) {
468 rcube::raise_error(array(
469 'code' => 600, 'type' => 'php',
470 'file' => __FILE__, 'line' => __LINE__,
471 'message' => "Error saving event object to Kolab server"),
472 true, false);
473 $saved = false;
474 }
475 else {
476 // save links in configuration.relation object
477 if ($this->save_links($event['uid'], $links)) {
478 $object['links'] = $links;
479 }
480
481 $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
482 }
483
484 return $saved;
485 }
486
487 /**
488 * Update a specific event record
489 *
490 * @see calendar_driver::new_event()
491 * @return boolean True on success, False on error
492 */
493
494 public function update_event($event, $exception_id = null)
495 {
496 $updated = false;
497 $old = $this->storage->get_object($event['uid'] ?: $event['id']);
498 if (!$old || PEAR::isError($old))
499 return false;
500
501 // email links are stored separately
502 $links = $event['links'];
503 unset($event['links']);
504
505 $object = $this->_from_driver_event($event, $old);
506 $saved = $this->storage->save($object, 'event', $old['uid']);
507
508 if (!$saved) {
509 rcube::raise_error(array(
510 'code' => 600, 'type' => 'php',
511 'file' => __FILE__, 'line' => __LINE__,
512 'message' => "Error saving event object to Kolab server"),
513 true, false);
514 }
515 else {
516 // save links in configuration.relation object
517 if ($this->save_links($event['uid'], $links)) {
518 $object['links'] = $links;
519 }
520
521 $updated = true;
522 $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
523
524 // refresh local cache with recurring instances
525 if ($exception_id) {
526 $this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
527 }
528 }
529
530 return $updated;
531 }
532
533 /**
534 * Delete an event record
535 *
536 * @see calendar_driver::remove_event()
537 * @return boolean True on success, False on error
538 */
539 public function delete_event($event, $force = true)
540 {
541 $deleted = $this->storage->delete($event['uid'] ?: $event['id'], $force);
542
543 if (!$deleted) {
544 rcube::raise_error(array(
545 'code' => 600, 'type' => 'php',
546 'file' => __FILE__, 'line' => __LINE__,
547 'message' => sprintf("Error deleting event object '%s' from Kolab server", $event['id'])),
548 true, false);
549 }
550
551 return $deleted;
552 }
553
554 /**
555 * Restore deleted event record
556 *
557 * @see calendar_driver::undelete_event()
558 * @return boolean True on success, False on error
559 */
560 public function restore_event($event)
561 {
562 if ($this->storage->undelete($event['id'])) {
563 return true;
564 }
565 else {
566 rcube::raise_error(array(
567 'code' => 600, 'type' => 'php',
568 'file' => __FILE__, 'line' => __LINE__,
569 'message' => "Error undeleting the event object $event[id] from the Kolab server"),
570 true, false);
571 }
572
573 return false;
574 }
575
576 /**
577 * Find messages linked with an event
578 */
579 protected function get_links($uid)
580 {
581 $storage = kolab_storage_config::get_instance();
582 return $storage->get_object_links($uid);
583 }
584
585 /**
586 *
587 */
588 protected function save_links($uid, $links)
589 {
590 $storage = kolab_storage_config::get_instance();
591 return $storage->save_object_links($uid, (array) $links);
592 }
593
594 /**
595 * Create instances of a recurring event
596 *
597 * @param array Hash array with event properties
598 * @param object DateTime Start date of the recurrence window
599 * @param object DateTime End date of the recurrence window
600 * @param string ID of a specific recurring event instance
601 * @return array List of recurring event instances
602 */
603 public function get_recurring_events($event, $start, $end = null, $event_id = null)
604 {
605 $object = $event['_formatobj'];
606 if (!$object) {
607 $rec = $this->storage->get_object($event['id']);
608 $object = $rec['_formatobj'];
609 }
610 if (!is_object($object))
611 return array();
612
613 // determine a reasonable end date if none given
614 if (!$end) {
615 $end = clone $event['start'];
616 $end->add(new DateInterval('P100Y'));
617 }
618
619 // copy the recurrence rule from the master event (to be used in the UI)
620 $recurrence_rule = $event['recurrence'];
621 unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
622
623 // read recurrence exceptions first
624 $events = array();
625 $exdata = array();
626 $futuredata = array();
627 $recurrence_id_format = libcalendaring::recurrence_id_format($event);
628
629 if (is_array($event['recurrence']['EXCEPTIONS'])) {
630 foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
631 if (!$exception['_instance'])
632 $exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, $event['allday']);
633
634 $rec_event = $this->_to_driver_event($exception, false, false, $event);
635 $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
636 $rec_event['isexception'] = 1;
637
638 // found the specifically requested instance: register exception (single occurrence wins)
639 if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
640 $rec_event['recurrence'] = $recurrence_rule;
641 $rec_event['recurrence_id'] = $event['uid'];
642 $this->events[$rec_event['id']] = $rec_event;
643 }
644
645 // remember this exception's date
646 $exdate = substr($exception['_instance'], 0, 8);
647 if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
648 $exdata[$exdate] = $rec_event;
649 }
650 if ($rec_event['thisandfuture']) {
651 $futuredata[$exdate] = $rec_event;
652 }
653 }
654 }
655
656 // found the specifically requested instance, exiting...
657 if ($event_id && !empty($this->events[$event_id])) {
658 return array($this->events[$event_id]);
659 }
660
661 // use libkolab to compute recurring events
662 if (class_exists('kolabcalendaring')) {
663 $recurrence = new kolab_date_recurrence($object);
664 }
665 else {
666 // fallback to local recurrence implementation
667 require_once($this->cal->home . '/lib/calendar_recurrence.php');
668 $recurrence = new calendar_recurrence($this->cal, $event);
669 }
670
671 $i = 0;
672 while ($next_event = $recurrence->next_instance()) {
673 $datestr = $next_event['start']->format('Ymd');
674 $instance_id = $next_event['start']->format($recurrence_id_format);
675
676 // use this event data for future recurring instances
677 if ($futuredata[$datestr])
678 $overlay_data = $futuredata[$datestr];
679
680 $rec_id = $event['uid'] . '-' . $instance_id;
681 $exception = $exdata[$datestr] ?: $overlay_data;
682 $event_start = $next_event['start'];
683 $event_end = $next_event['end'];
684
685 // copy some event from exception to get proper start/end dates
686 if ($exception) {
687 $event_copy = $next_event;
688 kolab_driver::merge_exception_dates($event_copy, $exception);
689 $event_start = $event_copy['start'];
690 $event_end = $event_copy['end'];
691 }
692
693 // add to output if in range
694 if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
695 $rec_event = $this->_to_driver_event($next_event, false, false, $event);
696 $rec_event['_instance'] = $instance_id;
697 $rec_event['_count'] = $i + 1;
698
699 if ($exception) // copy data from exception
700 kolab_driver::merge_exception_data($rec_event, $exception);
701
702 $rec_event['id'] = $rec_id;
703 $rec_event['recurrence_id'] = $event['uid'];
704 $rec_event['recurrence'] = $recurrence_rule;
705 unset($rec_event['_attendees']);
706 $events[] = $rec_event;
707
708 if ($rec_id == $event_id) {
709 $this->events[$rec_id] = $rec_event;
710 break;
711 }
712 }
713 else if ($next_event['start'] > $end) // stop loop if out of range
714 break;
715
716 // avoid endless recursion loops
717 if (++$i > 100000)
718 break;
719 }
720
721 return $events;
722 }
723
724 /**
725 * Convert from Kolab_Format to internal representation
726 */
727 private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
728 {
729 $record['calendar'] = $this->id;
730
731 if ($links && !array_key_exists('links', $record)) {
732 $record['links'] = $this->get_links($record['uid']);
733 }
734
735 if ($this->get_namespace() == 'other') {
736 $record['className'] = 'fc-event-ns-other';
737 $record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION','DECLINED'), $this->get_owner());
738 }
739
740 // add instance identifier to first occurrence (master event)
741 $recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
742 if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
743 $record['_instance'] = $record['start']->format($recurrence_id_format);
744 }
745 else if (is_a($record['recurrence_date'], 'DateTime')) {
746 $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
747 }
748
749 // clean up exception data
750 if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) {
751 array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
752 unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
753 });
754 }
755
756 return $record;
757 }
758
759 /**
760 * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
761 * (opposite of self::_to_driver_event())
762 */
763 private function _from_driver_event($event, $old = array())
764 {
765 // set current user as ORGANIZER
766 if ($identity = $this->cal->rc->user->list_emails(true)) {
767 $event['attendees'] = (array) $event['attendees'];
768 $found = false;
769
770 // there can be only resources on attendees list (T1484)
771 // let's check the existence of an organizer
772 foreach ($event['attendees'] as $attendee) {
773 if ($attendee['role'] == 'ORGANIZER') {
774 $found = true;
775 break;
776 }
777 }
778
779 if (!$found) {
780 $event['attendees'][] = array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']);
781 }
782
783 $event['_owner'] = $identity['email'];
784 }
785
786 // remove EXDATE values if RDATE is given
787 if (!empty($event['recurrence']['RDATE'])) {
788 $event['recurrence']['EXDATE'] = array();
789 }
790
791 // remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
792 if ($event['recurrence'] && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
793 $event['recurrence'] = array();
794 }
795
796 // keep 'comment' from initial itip invitation
797 if (!empty($old['comment'])) {
798 $event['comment'] = $old['comment'];
799 }
800
801 // clean up exception data
802 if (is_array($event['exceptions'])) {
803 array_walk($event['exceptions'], function(&$exception) {
804 unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments'],
805 $event['attachments'], $event['deleted_attachments'], $event['recurrence_id']);
806 });
807 }
808
809 // remove some internal properties which should not be saved
810 unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
811 $event['recurrence_id'], $event['attachments'], $event['deleted_attachments'], $event['className']);
812
813 // copy meta data (starting with _) from old object
814 foreach ((array)$old as $key => $val) {
815 if (!isset($event[$key]) && $key[0] == '_')
816 $event[$key] = $val;
817 }
818
819 return $event;
820 }
821
822 /**
823 * Match the given word in the event contents
824 */
825 public function fulltext_match($event, $word, $recursive = true)
826 {
827 $hits = 0;
828 foreach ($this->search_fields as $col) {
829 $sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
830 if (empty($sval))
831 continue;
832
833 // do a simple substring matching (to be improved)
834 $val = mb_strtolower($sval);
835 if (strpos($val, $word) !== false) {
836 $hits++;
837 break;
838 }
839 }
840
841 return $hits;
842 }
843
844 /**
845 * Convert a complex event attribute to a string value
846 */
847 private static function _complex2string($prop)
848 {
849 static $ignorekeys = array('role','status','rsvp');
850
851 $out = '';
852 if (is_array($prop)) {
853 foreach ($prop as $key => $val) {
854 if (is_numeric($key)) {
855 $out .= self::_complex2string($val);
856 }
857 else if (!in_array($key, $ignorekeys)) {
858 $out .= $val . ' ';
859 }
860 }
861 }
862 else if (is_string($prop) || is_numeric($prop)) {
863 $out .= $prop . ' ';
864 }
865
866 return rtrim($out);
867 }
868
869 }