3
|
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 }
|