Mercurial > hg > rc1
comparison plugins/calendar/drivers/database/database_driver.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 * Database driver for the Calendar plugin | |
5 * | |
6 * @author Lazlo Westerhof <hello@lazlo.me> | |
7 * @author Thomas Bruederli <bruederli@kolabsys.com> | |
8 * | |
9 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> | |
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 database_driver extends calendar_driver | |
28 { | |
29 const DB_DATE_FORMAT = 'Y-m-d H:i:s'; | |
30 | |
31 public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled'); | |
32 | |
33 // features this backend supports | |
34 public $alarms = true; | |
35 public $attendees = true; | |
36 public $freebusy = false; | |
37 public $attachments = true; | |
38 public $alarm_types = array('DISPLAY'); | |
39 | |
40 private $rc; | |
41 private $cal; | |
42 private $cache = array(); | |
43 private $calendars = array(); | |
44 private $calendar_ids = ''; | |
45 private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3); | |
46 private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2); | |
47 private $server_timezone; | |
48 | |
49 private $db_events = 'events'; | |
50 private $db_calendars = 'calendars'; | |
51 private $db_attachments = 'attachments'; | |
52 | |
53 | |
54 /** | |
55 * Default constructor | |
56 */ | |
57 public function __construct($cal) | |
58 { | |
59 $this->cal = $cal; | |
60 $this->rc = $cal->rc; | |
61 $this->server_timezone = new DateTimeZone(date_default_timezone_get()); | |
62 | |
63 // read database config | |
64 $db = $this->rc->get_dbh(); | |
65 $this->db_events = $this->rc->config->get('db_table_events', $db->table_name($this->db_events)); | |
66 $this->db_calendars = $this->rc->config->get('db_table_calendars', $db->table_name($this->db_calendars)); | |
67 $this->db_attachments = $this->rc->config->get('db_table_attachments', $db->table_name($this->db_attachments)); | |
68 | |
69 $this->_read_calendars(); | |
70 } | |
71 | |
72 /** | |
73 * Read available calendars for the current user and store them internally | |
74 */ | |
75 private function _read_calendars() | |
76 { | |
77 $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); | |
78 | |
79 if (!empty($this->rc->user->ID)) { | |
80 $calendar_ids = array(); | |
81 $result = $this->rc->db->query( | |
82 "SELECT *, calendar_id AS id FROM " . $this->db_calendars . " | |
83 WHERE user_id=? | |
84 ORDER BY name", | |
85 $this->rc->user->ID | |
86 ); | |
87 while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { | |
88 $arr['showalarms'] = intval($arr['showalarms']); | |
89 $arr['active'] = !in_array($arr['id'], $hidden); | |
90 $arr['name'] = html::quote($arr['name']); | |
91 $arr['listname'] = html::quote($arr['name']); | |
92 $arr['rights'] = 'lrswikxteav'; | |
93 $arr['editable'] = true; | |
94 $this->calendars[$arr['calendar_id']] = $arr; | |
95 $calendar_ids[] = $this->rc->db->quote($arr['calendar_id']); | |
96 } | |
97 $this->calendar_ids = join(',', $calendar_ids); | |
98 } | |
99 } | |
100 | |
101 /** | |
102 * Get a list of available calendars from this source | |
103 * | |
104 * @param integer Bitmask defining filter criterias | |
105 * | |
106 * @return array List of calendars | |
107 */ | |
108 public function list_calendars($filter = 0) | |
109 { | |
110 // attempt to create a default calendar for this user | |
111 if (empty($this->calendars)) { | |
112 if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000', 'showalarms' => true))) | |
113 $this->_read_calendars(); | |
114 } | |
115 | |
116 $calendars = $this->calendars; | |
117 | |
118 // filter active calendars | |
119 if ($filter & self::FILTER_ACTIVE) { | |
120 foreach ($calendars as $idx => $cal) { | |
121 if (!$cal['active']) { | |
122 unset($calendars[$idx]); | |
123 } | |
124 } | |
125 } | |
126 | |
127 // 'personal' is unsupported in this driver | |
128 | |
129 // append the virtual birthdays calendar | |
130 if ($this->rc->config->get('calendar_contact_birthdays', false)) { | |
131 $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); | |
132 $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); | |
133 | |
134 $id = self::BIRTHDAY_CALENDAR_ID; | |
135 if (!$active || !in_array($id, $hidden)) { | |
136 $calendars[$id] = array( | |
137 'id' => $id, | |
138 'name' => $this->cal->gettext('birthdays'), | |
139 'listname' => $this->cal->gettext('birthdays'), | |
140 'color' => $prefs['color'], | |
141 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'), | |
142 'active' => !in_array($id, $hidden), | |
143 'group' => 'x-birthdays', | |
144 'editable' => false, | |
145 'default' => false, | |
146 'children' => false, | |
147 ); | |
148 } | |
149 } | |
150 | |
151 return $calendars; | |
152 } | |
153 | |
154 /** | |
155 * Create a new calendar assigned to the current user | |
156 * | |
157 * @param array Hash array with calendar properties | |
158 * name: Calendar name | |
159 * color: The color of the calendar | |
160 * @return mixed ID of the calendar on success, False on error | |
161 */ | |
162 public function create_calendar($prop) | |
163 { | |
164 $result = $this->rc->db->query( | |
165 "INSERT INTO " . $this->db_calendars . " | |
166 (user_id, name, color, showalarms) | |
167 VALUES (?, ?, ?, ?)", | |
168 $this->rc->user->ID, | |
169 $prop['name'], | |
170 $prop['color'], | |
171 $prop['showalarms']?1:0 | |
172 ); | |
173 | |
174 if ($result) | |
175 return $this->rc->db->insert_id($this->db_calendars); | |
176 | |
177 return false; | |
178 } | |
179 | |
180 /** | |
181 * Update properties of an existing calendar | |
182 * | |
183 * @see calendar_driver::edit_calendar() | |
184 */ | |
185 public function edit_calendar($prop) | |
186 { | |
187 // birthday calendar properties are saved in user prefs | |
188 if ($prop['id'] == self::BIRTHDAY_CALENDAR_ID) { | |
189 $prefs['birthday_calendar'] = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); | |
190 if (isset($prop['color'])) | |
191 $prefs['birthday_calendar']['color'] = $prop['color']; | |
192 if (isset($prop['showalarms'])) | |
193 $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : ''; | |
194 $this->rc->user->save_prefs($prefs); | |
195 return true; | |
196 } | |
197 | |
198 $query = $this->rc->db->query( | |
199 "UPDATE " . $this->db_calendars . " | |
200 SET name=?, color=?, showalarms=? | |
201 WHERE calendar_id=? | |
202 AND user_id=?", | |
203 $prop['name'], | |
204 $prop['color'], | |
205 $prop['showalarms']?1:0, | |
206 $prop['id'], | |
207 $this->rc->user->ID | |
208 ); | |
209 | |
210 return $this->rc->db->affected_rows($query); | |
211 } | |
212 | |
213 /** | |
214 * Set active/subscribed state of a calendar | |
215 * Save a list of hidden calendars in user prefs | |
216 * | |
217 * @see calendar_driver::subscribe_calendar() | |
218 */ | |
219 public function subscribe_calendar($prop) | |
220 { | |
221 $hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', ''))); | |
222 | |
223 if ($prop['active']) | |
224 unset($hidden[$prop['id']]); | |
225 else | |
226 $hidden[$prop['id']] = 1; | |
227 | |
228 return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden)))); | |
229 } | |
230 | |
231 /** | |
232 * Delete the given calendar with all its contents | |
233 * | |
234 * @see calendar_driver::delete_calendar() | |
235 */ | |
236 public function delete_calendar($prop) | |
237 { | |
238 if (!$this->calendars[$prop['id']]) | |
239 return false; | |
240 | |
241 // events and attachments will be deleted by foreign key cascade | |
242 | |
243 $query = $this->rc->db->query( | |
244 "DELETE FROM " . $this->db_calendars . " | |
245 WHERE calendar_id=?", | |
246 $prop['id'] | |
247 ); | |
248 | |
249 return $this->rc->db->affected_rows($query); | |
250 } | |
251 | |
252 /** | |
253 * Search for shared or otherwise not listed calendars the user has access | |
254 * | |
255 * @param string Search string | |
256 * @param string Section/source to search | |
257 * @return array List of calendars | |
258 */ | |
259 public function search_calendars($query, $source) | |
260 { | |
261 // not implemented | |
262 return array(); | |
263 } | |
264 | |
265 /** | |
266 * Add a single event to the database | |
267 * | |
268 * @param array Hash array with event properties | |
269 * @see calendar_driver::new_event() | |
270 */ | |
271 public function new_event($event) | |
272 { | |
273 if (!$this->validate($event)) | |
274 return false; | |
275 | |
276 if (!empty($this->calendars)) { | |
277 if ($event['calendar'] && !$this->calendars[$event['calendar']]) | |
278 return false; | |
279 if (!$event['calendar']) | |
280 $event['calendar'] = reset(array_keys($this->calendars)); | |
281 | |
282 if ($event_id = $this->_insert_event($event)) { | |
283 $this->_update_recurring($event); | |
284 } | |
285 | |
286 return $event_id; | |
287 } | |
288 | |
289 return false; | |
290 } | |
291 | |
292 /** | |
293 * | |
294 */ | |
295 private function _insert_event(&$event) | |
296 { | |
297 $event = $this->_save_preprocess($event); | |
298 | |
299 $this->rc->db->query(sprintf( | |
300 "INSERT INTO " . $this->db_events . " | |
301 (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence, | |
302 title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat) | |
303 VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
304 $this->rc->db->quote_identifier('start'), | |
305 $this->rc->db->quote_identifier('end'), | |
306 $this->rc->db->now(), | |
307 $this->rc->db->now() | |
308 ), | |
309 $event['calendar'], | |
310 strval($event['uid']), | |
311 intval($event['recurrence_id']), | |
312 strval($event['_instance']), | |
313 intval($event['isexception']), | |
314 $event['start']->format(self::DB_DATE_FORMAT), | |
315 $event['end']->format(self::DB_DATE_FORMAT), | |
316 intval($event['all_day']), | |
317 $event['_recurrence'], | |
318 strval($event['title']), | |
319 strval($event['description']), | |
320 strval($event['location']), | |
321 join(',', (array)$event['categories']), | |
322 strval($event['url']), | |
323 intval($event['free_busy']), | |
324 intval($event['priority']), | |
325 intval($event['sensitivity']), | |
326 strval($event['status']), | |
327 $event['attendees'], | |
328 $event['alarms'], | |
329 $event['notifyat'] | |
330 ); | |
331 | |
332 $event_id = $this->rc->db->insert_id($this->db_events); | |
333 | |
334 if ($event_id) { | |
335 $event['id'] = $event_id; | |
336 | |
337 // add attachments | |
338 if (!empty($event['attachments'])) { | |
339 foreach ($event['attachments'] as $attachment) { | |
340 $this->add_attachment($attachment, $event_id); | |
341 unset($attachment); | |
342 } | |
343 } | |
344 | |
345 return $event_id; | |
346 } | |
347 | |
348 return false; | |
349 } | |
350 | |
351 /** | |
352 * Update an event entry with the given data | |
353 * | |
354 * @param array Hash array with event properties | |
355 * @see calendar_driver::edit_event() | |
356 */ | |
357 public function edit_event($event) | |
358 { | |
359 if (!empty($this->calendars)) { | |
360 $update_master = false; | |
361 $update_recurring = true; | |
362 $old = $this->get_event($event); | |
363 $ret = true; | |
364 | |
365 // check if update affects scheduling and update attendee status accordingly | |
366 $reschedule = $this->_check_scheduling($event, $old, true); | |
367 | |
368 // increment sequence number | |
369 if (empty($event['sequence']) && $reschedule) | |
370 $event['sequence'] = max($event['sequence'], $old['sequence']) + 1; | |
371 | |
372 // modify a recurring event, check submitted savemode to do the right things | |
373 if ($old['recurrence'] || $old['recurrence_id']) { | |
374 $master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old; | |
375 | |
376 // keep saved exceptions (not submitted by the client) | |
377 if ($old['recurrence']['EXDATE']) | |
378 $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; | |
379 | |
380 switch ($event['_savemode']) { | |
381 case 'new': | |
382 $event['uid'] = $this->cal->generate_uid(); | |
383 return $this->new_event($event); | |
384 | |
385 case 'current': | |
386 // save as exception | |
387 $event['isexception'] = 1; | |
388 $update_recurring = false; | |
389 | |
390 // set exception to first instance (= master) | |
391 if ($event['id'] == $master['id']) { | |
392 $event += $old; | |
393 $event['recurrence_id'] = $master['id']; | |
394 $event['_instance'] = libcalendaring::recurrence_instance_identifier($old, $master['allday']); | |
395 $event['isexception'] = 1; | |
396 $event_id = $this->_insert_event($event); | |
397 return $event_id; | |
398 } | |
399 break; | |
400 | |
401 case 'future': | |
402 if ($master['id'] != $event['id']) { | |
403 // set until-date on master event, then save this instance as new recurring event | |
404 $master['recurrence']['UNTIL'] = clone $event['start']; | |
405 $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); | |
406 unset($master['recurrence']['COUNT']); | |
407 $update_master = true; | |
408 | |
409 // if recurrence COUNT, update value to the correct number of future occurences | |
410 if ($event['recurrence']['COUNT']) { | |
411 $fromdate = clone $event['start']; | |
412 $fromdate->setTimezone($this->server_timezone); | |
413 $sqlresult = $this->rc->db->query(sprintf( | |
414 "SELECT event_id FROM " . $this->db_events . " | |
415 WHERE calendar_id IN (%s) | |
416 AND %s >= ? | |
417 AND recurrence_id=?", | |
418 $this->calendar_ids, | |
419 $this->rc->db->quote_identifier('start') | |
420 ), | |
421 $fromdate->format(self::DB_DATE_FORMAT), | |
422 $master['id']); | |
423 if ($count = $this->rc->db->num_rows($sqlresult)) | |
424 $event['recurrence']['COUNT'] = $count; | |
425 } | |
426 | |
427 $update_recurring = true; | |
428 $event['recurrence_id'] = 0; | |
429 $event['isexception'] = 0; | |
430 $event['_instance'] = ''; | |
431 break; | |
432 } | |
433 // else: 'future' == 'all' if modifying the master event | |
434 | |
435 default: // 'all' is default | |
436 $event['id'] = $master['id']; | |
437 $event['recurrence_id'] = 0; | |
438 | |
439 // use start date from master but try to be smart on time or duration changes | |
440 $old_start_date = $old['start']->format('Y-m-d'); | |
441 $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i'); | |
442 $old_duration = $old['end']->format('U') - $old['start']->format('U'); | |
443 | |
444 $new_start_date = $event['start']->format('Y-m-d'); | |
445 $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i'); | |
446 $new_duration = $event['end']->format('U') - $event['start']->format('U'); | |
447 | |
448 $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; | |
449 $date_shift = $old['start']->diff($event['start']); | |
450 | |
451 // shifted or resized | |
452 if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { | |
453 $event['start'] = $master['start']->add($old['start']->diff($event['start'])); | |
454 $event['end'] = clone $event['start']; | |
455 $event['end']->add(new DateInterval('PT'.$new_duration.'S')); | |
456 } | |
457 // dates did not change, use the ones from master | |
458 else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) { | |
459 $event['start'] = $master['start']; | |
460 $event['end'] = $master['end']; | |
461 } | |
462 | |
463 // adjust recurrence-id when start changed and therefore the entire recurrence chain changes | |
464 if (is_array($event['recurrence']) && ($old_start_date != $new_start_date || $old_start_time != $new_start_time) | |
465 && ($exceptions = $this->_load_exceptions($old))) { | |
466 $recurrence_id_format = libcalendaring::recurrence_id_format($event); | |
467 foreach ($exceptions as $exception) { | |
468 $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone()); | |
469 if (is_a($recurrence_id, 'DateTime')) { | |
470 $recurrence_id->add($date_shift); | |
471 $exception['_instance'] = $recurrence_id->format($recurrence_id_format); | |
472 $this->_update_event($exception, false); | |
473 } | |
474 } | |
475 } | |
476 | |
477 $ret = $event['id']; // return master ID | |
478 break; | |
479 } | |
480 } | |
481 | |
482 $success = $this->_update_event($event, $update_recurring); | |
483 | |
484 if ($success && $update_master) | |
485 $this->_update_event($master, true); | |
486 | |
487 return $success ? $ret : false; | |
488 } | |
489 | |
490 return false; | |
491 } | |
492 | |
493 /** | |
494 * Extended event editing with possible changes to the argument | |
495 * | |
496 * @param array Hash array with event properties | |
497 * @param string New participant status | |
498 * @param array List of hash arrays with updated attendees | |
499 * @return boolean True on success, False on error | |
500 */ | |
501 public function edit_rsvp(&$event, $status, $attendees) | |
502 { | |
503 $update_event = $event; | |
504 | |
505 // apply changes to master (and all exceptions) | |
506 if ($event['_savemode'] == 'all' && $event['recurrence_id']) { | |
507 $update_event = $this->get_event(array('id' => $event['recurrence_id'])); | |
508 $update_event['_savemode'] = $event['_savemode']; | |
509 calendar::merge_attendee_data($update_event, $attendees); | |
510 } | |
511 | |
512 if ($ret = $this->update_attendees($update_event, $attendees)) { | |
513 // replace $event with effectively updated event (for iTip reply) | |
514 if ($ret !== true && $ret != $update_event['id'] && ($new_event = $this->get_event(array('id' => $ret)))) { | |
515 $event = $new_event; | |
516 } | |
517 else { | |
518 $event = $update_event; | |
519 } | |
520 } | |
521 | |
522 return $ret; | |
523 } | |
524 | |
525 /** | |
526 * Update the participant status for the given attendees | |
527 * | |
528 * @see calendar_driver::update_attendees() | |
529 */ | |
530 public function update_attendees(&$event, $attendees) | |
531 { | |
532 $success = $this->edit_event($event, true); | |
533 | |
534 // apply attendee updates to recurrence exceptions too | |
535 if ($success && $event['_savemode'] == 'all' && !empty($event['recurrence']) && empty($event['recurrence_id']) && ($exceptions = $this->_load_exceptions($event))) { | |
536 foreach ($exceptions as $exception) { | |
537 calendar::merge_attendee_data($exception, $attendees); | |
538 $this->_update_event($exception, false); | |
539 } | |
540 } | |
541 | |
542 return $success; | |
543 } | |
544 | |
545 /** | |
546 * Determine whether the current change affects scheduling and reset attendee status accordingly | |
547 */ | |
548 private function _check_scheduling(&$event, $old, $update = true) | |
549 { | |
550 // skip this check when importing iCal/iTip events | |
551 if (isset($event['sequence']) || !empty($event['_method'])) { | |
552 return false; | |
553 } | |
554 | |
555 $reschedule = false; | |
556 | |
557 // iterate through the list of properties considered 'significant' for scheduling | |
558 foreach (self::$scheduling_properties as $prop) { | |
559 $a = $old[$prop]; | |
560 $b = $event[$prop]; | |
561 if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) { | |
562 $a = $a->format('Y-m-d'); | |
563 $b = $b->format('Y-m-d'); | |
564 } | |
565 if ($prop == 'recurrence' && is_array($a) && is_array($b)) { | |
566 unset($a['EXCEPTIONS'], $b['EXCEPTIONS']); | |
567 $a = array_filter($a); | |
568 $b = array_filter($b); | |
569 | |
570 // advanced rrule comparison: no rescheduling if series was shortened | |
571 if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) { | |
572 unset($a['COUNT'], $b['COUNT']); | |
573 } | |
574 else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) { | |
575 unset($a['UNTIL'], $b['UNTIL']); | |
576 } | |
577 } | |
578 if ($a != $b) { | |
579 $reschedule = true; | |
580 break; | |
581 } | |
582 } | |
583 | |
584 // reset all attendee status to needs-action (#4360) | |
585 if ($update && $reschedule && is_array($event['attendees'])) { | |
586 $is_organizer = false; | |
587 $emails = $this->cal->get_user_emails(); | |
588 $attendees = $event['attendees']; | |
589 foreach ($attendees as $i => $attendee) { | |
590 if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { | |
591 $is_organizer = true; | |
592 } | |
593 else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') { | |
594 $attendees[$i]['status'] = 'NEEDS-ACTION'; | |
595 $attendees[$i]['rsvp'] = true; | |
596 } | |
597 } | |
598 | |
599 // update attendees only if I'm the organizer | |
600 if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) { | |
601 $event['attendees'] = $attendees; | |
602 } | |
603 } | |
604 | |
605 return $reschedule; | |
606 } | |
607 | |
608 /** | |
609 * Convert save data to be used in SQL statements | |
610 */ | |
611 private function _save_preprocess($event) | |
612 { | |
613 // shift dates to server's timezone (except for all-day events) | |
614 if (!$event['allday']) { | |
615 $event['start'] = clone $event['start']; | |
616 $event['start']->setTimezone($this->server_timezone); | |
617 $event['end'] = clone $event['end']; | |
618 $event['end']->setTimezone($this->server_timezone); | |
619 } | |
620 | |
621 // compose vcalendar-style recurrencue rule from structured data | |
622 $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : ''; | |
623 $event['_recurrence'] = rtrim($rrule, ';'); | |
624 $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]); | |
625 $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]); | |
626 | |
627 if ($event['free_busy'] == 'tentative') { | |
628 $event['status'] = 'TENTATIVE'; | |
629 } | |
630 | |
631 if (isset($event['allday'])) { | |
632 $event['all_day'] = $event['allday'] ? 1 : 0; | |
633 } | |
634 | |
635 // compute absolute time to notify the user | |
636 $event['notifyat'] = $this->_get_notification($event); | |
637 | |
638 if (is_array($event['valarms'])) { | |
639 $event['alarms'] = $this->serialize_alarms($event['valarms']); | |
640 } | |
641 | |
642 // process event attendees | |
643 if (!empty($event['attendees'])) | |
644 $event['attendees'] = json_encode((array)$event['attendees']); | |
645 else | |
646 $event['attendees'] = ''; | |
647 | |
648 return $event; | |
649 } | |
650 | |
651 /** | |
652 * Compute absolute time to notify the user | |
653 */ | |
654 private function _get_notification($event) | |
655 { | |
656 if ($event['valarms'] && $event['start'] > new DateTime()) { | |
657 $alarm = libcalendaring::get_next_alarm($event); | |
658 | |
659 if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) | |
660 return date('Y-m-d H:i:s', $alarm['time']); | |
661 } | |
662 | |
663 return null; | |
664 } | |
665 | |
666 /** | |
667 * Save the given event record to database | |
668 * | |
669 * @param array Event data | |
670 * @param boolean True if recurring events instances should be updated, too | |
671 */ | |
672 private function _update_event($event, $update_recurring = true) | |
673 { | |
674 $event = $this->_save_preprocess($event); | |
675 $sql_set = array(); | |
676 $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat'); | |
677 foreach ($set_cols as $col) { | |
678 if (is_object($event[$col]) && is_a($event[$col], 'DateTime')) | |
679 $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT)); | |
680 else if (is_array($event[$col])) | |
681 $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col])); | |
682 else if (array_key_exists($col, $event)) | |
683 $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]); | |
684 } | |
685 | |
686 if ($event['_recurrence']) | |
687 $sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']); | |
688 | |
689 if ($event['_instance']) | |
690 $sql_set[] = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event['_instance']); | |
691 | |
692 if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) | |
693 $sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']); | |
694 | |
695 $query = $this->rc->db->query(sprintf( | |
696 "UPDATE " . $this->db_events . " | |
697 SET changed=%s %s | |
698 WHERE event_id=? | |
699 AND calendar_id IN (" . $this->calendar_ids . ")", | |
700 $this->rc->db->now(), | |
701 ($sql_set ? ', ' . join(', ', $sql_set) : '') | |
702 ), | |
703 $event['id'] | |
704 ); | |
705 | |
706 $success = $this->rc->db->affected_rows($query); | |
707 | |
708 // add attachments | |
709 if ($success && !empty($event['attachments'])) { | |
710 foreach ($event['attachments'] as $attachment) { | |
711 $this->add_attachment($attachment, $event['id']); | |
712 unset($attachment); | |
713 } | |
714 } | |
715 | |
716 // remove attachments | |
717 if ($success && !empty($event['deleted_attachments'])) { | |
718 foreach ($event['deleted_attachments'] as $attachment) { | |
719 $this->remove_attachment($attachment, $event['id']); | |
720 } | |
721 } | |
722 | |
723 if ($success) { | |
724 unset($this->cache[$event['id']]); | |
725 if ($update_recurring) | |
726 $this->_update_recurring($event); | |
727 } | |
728 | |
729 return $success; | |
730 } | |
731 | |
732 /** | |
733 * Insert "fake" entries for recurring occurences of this event | |
734 */ | |
735 private function _update_recurring($event) | |
736 { | |
737 if (empty($this->calendars)) | |
738 return; | |
739 | |
740 if (!empty($event['recurrence'])) { | |
741 $exdata = array(); | |
742 $exceptions = $this->_load_exceptions($event); | |
743 | |
744 foreach ($exceptions as $exception) { | |
745 $exdate = substr($exception['_instance'], 0, 8); | |
746 $exdata[$exdate] = $exception; | |
747 } | |
748 } | |
749 | |
750 // clear existing recurrence copies | |
751 $this->rc->db->query( | |
752 "DELETE FROM " . $this->db_events . " | |
753 WHERE recurrence_id=? | |
754 AND isexception=0 | |
755 AND calendar_id IN (" . $this->calendar_ids . ")", | |
756 $event['id'] | |
757 ); | |
758 | |
759 // create new fake entries | |
760 if (!empty($event['recurrence'])) { | |
761 // include library class | |
762 require_once($this->cal->home . '/lib/calendar_recurrence.php'); | |
763 | |
764 $recurrence = new calendar_recurrence($this->cal, $event); | |
765 | |
766 $count = 0; | |
767 $event['allday'] = $event['all_day']; | |
768 $duration = $event['start']->diff($event['end']); | |
769 $recurrence_id_format = libcalendaring::recurrence_id_format($event); | |
770 while ($next_start = $recurrence->next_start()) { | |
771 $instance = $next_start->format($recurrence_id_format); | |
772 $datestr = substr($instance, 0, 8); | |
773 | |
774 // skip exceptions | |
775 // TODO: merge updated data from master event | |
776 if ($exdata[$datestr]) { | |
777 continue; | |
778 } | |
779 | |
780 $next_start->setTimezone($this->server_timezone); | |
781 $next_end = clone $next_start; | |
782 $next_end->add($duration); | |
783 | |
784 $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status'])); | |
785 $query = $this->rc->db->query(sprintf( | |
786 "INSERT INTO " . $this->db_events . " | |
787 (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat) | |
788 SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ? | |
789 FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")", | |
790 $this->rc->db->quote_identifier('start'), | |
791 $this->rc->db->quote_identifier('end'), | |
792 $this->rc->db->now(), | |
793 $this->rc->db->now() | |
794 ), | |
795 $event['id'], | |
796 $instance, | |
797 $next_start->format(self::DB_DATE_FORMAT), | |
798 $next_end->format(self::DB_DATE_FORMAT), | |
799 $notify_at, | |
800 $event['id'] | |
801 ); | |
802 | |
803 if (!$this->rc->db->affected_rows($query)) | |
804 break; | |
805 | |
806 // stop adding events for inifinite recurrence after 20 years | |
807 if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20)) | |
808 break; | |
809 } | |
810 | |
811 // remove all exceptions after recurrence end | |
812 if ($next_end && !empty($exceptions)) { | |
813 $this->rc->db->query( | |
814 "DELETE FROM " . $this->db_events . " | |
815 WHERE `recurrence_id`=? | |
816 AND `isexception`=1 | |
817 AND `start` > ? | |
818 AND `calendar_id` IN (" . $this->calendar_ids . ")", | |
819 $event['id'], | |
820 $next_end->format(self::DB_DATE_FORMAT) | |
821 ); | |
822 } | |
823 } | |
824 } | |
825 | |
826 /** | |
827 * | |
828 */ | |
829 private function _load_exceptions($event, $instance_id = null) | |
830 { | |
831 $sql_add_where = ''; | |
832 if (!empty($instance_id)) { | |
833 $sql_add_where = 'AND `instance`=?'; | |
834 } | |
835 | |
836 $result = $this->rc->db->query( | |
837 "SELECT * FROM " . $this->db_events . " | |
838 WHERE `recurrence_id`=? | |
839 AND `isexception`=1 | |
840 AND `calendar_id` IN (" . $this->calendar_ids . ") | |
841 $sql_add_where | |
842 ORDER BY `instance`, `start`", | |
843 $event['id'], | |
844 $instance_id | |
845 ); | |
846 | |
847 $exceptions = array(); | |
848 while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) { | |
849 $exception = $this->_read_postprocess($sql_arr); | |
850 $instance = $exception['_instance'] ?: $exception['start']->format($exception['allday'] ? 'Ymd' : 'Ymd\THis'); | |
851 $exceptions[$instance] = $exception; | |
852 } | |
853 | |
854 return $exceptions; | |
855 } | |
856 | |
857 /** | |
858 * Move a single event | |
859 * | |
860 * @param array Hash array with event properties | |
861 * @see calendar_driver::move_event() | |
862 */ | |
863 public function move_event($event) | |
864 { | |
865 // let edit_event() do all the magic | |
866 return $this->edit_event($event + (array)$this->get_event($event)); | |
867 } | |
868 | |
869 /** | |
870 * Resize a single event | |
871 * | |
872 * @param array Hash array with event properties | |
873 * @see calendar_driver::resize_event() | |
874 */ | |
875 public function resize_event($event) | |
876 { | |
877 // let edit_event() do all the magic | |
878 return $this->edit_event($event + (array)$this->get_event($event)); | |
879 } | |
880 | |
881 /** | |
882 * Remove a single event from the database | |
883 * | |
884 * @param array Hash array with event properties | |
885 * @param boolean Remove record irreversible (@TODO) | |
886 * | |
887 * @see calendar_driver::remove_event() | |
888 */ | |
889 public function remove_event($event, $force = true) | |
890 { | |
891 if (!empty($this->calendars)) { | |
892 $event += (array)$this->get_event($event); | |
893 $master = $event; | |
894 $update_master = false; | |
895 $savemode = 'all'; | |
896 $ret = true; | |
897 | |
898 // read master if deleting a recurring event | |
899 if ($event['recurrence'] || $event['recurrence_id']) { | |
900 $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event; | |
901 $savemode = $event['_savemode']; | |
902 } | |
903 | |
904 switch ($savemode) { | |
905 case 'current': | |
906 // add exception to master event | |
907 $master['recurrence']['EXDATE'][] = $event['start']; | |
908 $update_master = true; | |
909 | |
910 // just delete this single occurence | |
911 $query = $this->rc->db->query( | |
912 "DELETE FROM " . $this->db_events . " | |
913 WHERE calendar_id IN (" . $this->calendar_ids . ") | |
914 AND event_id=?", | |
915 $event['id'] | |
916 ); | |
917 break; | |
918 | |
919 case 'future': | |
920 if ($master['id'] != $event['id']) { | |
921 // set until-date on master event | |
922 $master['recurrence']['UNTIL'] = clone $event['start']; | |
923 $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); | |
924 unset($master['recurrence']['COUNT']); | |
925 $update_master = true; | |
926 | |
927 // delete this and all future instances | |
928 $fromdate = clone $event['start']; | |
929 $fromdate->setTimezone($this->server_timezone); | |
930 $query = $this->rc->db->query( | |
931 "DELETE FROM " . $this->db_events . " | |
932 WHERE calendar_id IN (" . $this->calendar_ids . ") | |
933 AND " . $this->rc->db->quote_identifier('start') . " >= ? | |
934 AND recurrence_id=?", | |
935 $fromdate->format(self::DB_DATE_FORMAT), | |
936 $master['id'] | |
937 ); | |
938 $ret = $master['id']; | |
939 break; | |
940 } | |
941 // else: future == all if modifying the master event | |
942 | |
943 default: // 'all' is default | |
944 $query = $this->rc->db->query( | |
945 "DELETE FROM " . $this->db_events . " | |
946 WHERE (event_id=? OR recurrence_id=?) | |
947 AND calendar_id IN (" . $this->calendar_ids . ")", | |
948 $master['id'], | |
949 $master['id'] | |
950 ); | |
951 break; | |
952 } | |
953 | |
954 $success = $this->rc->db->affected_rows($query); | |
955 if ($success && $update_master) | |
956 $this->_update_event($master, true); | |
957 | |
958 return $success ? $ret : false; | |
959 } | |
960 | |
961 return false; | |
962 } | |
963 | |
964 /** | |
965 * Return data of a specific event | |
966 * @param mixed Hash array with event properties or event UID | |
967 * @param integer Bitmask defining the scope to search events in | |
968 * @param boolean If true, recurrence exceptions shall be added | |
969 * @return array Hash array with event properties | |
970 */ | |
971 public function get_event($event, $scope = 0, $full = false) | |
972 { | |
973 $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event; | |
974 $cal = is_array($event) ? $event['calendar'] : null; | |
975 $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid'; | |
976 | |
977 $where_add = ''; | |
978 if (is_array($event) && !$event['id'] && !empty($event['_instance'])) { | |
979 $where_add = 'AND instance=' . $this->rc->db->quote($event['_instance']); | |
980 } | |
981 | |
982 if ($this->cache[$id]) | |
983 return $this->cache[$id]; | |
984 | |
985 // get event from the address books birthday calendar | |
986 if ($cal == self::BIRTHDAY_CALENDAR_ID) { | |
987 return $this->get_birthday_event($id); | |
988 } | |
989 | |
990 if ($scope & self::FILTER_ACTIVE) { | |
991 $calendars = $this->calendars; | |
992 foreach ($calendars as $idx => $cal) { | |
993 if (!$cal['active']) { | |
994 unset($calendars[$idx]); | |
995 } | |
996 } | |
997 $cals = join(',', $calendars); | |
998 } | |
999 else { | |
1000 $cals = $this->calendar_ids; | |
1001 } | |
1002 | |
1003 $result = $this->rc->db->query(sprintf( | |
1004 "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . " | |
1005 WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments | |
1006 FROM " . $this->db_events . " AS e | |
1007 WHERE e.calendar_id IN (%s) | |
1008 AND e.$col=? | |
1009 %s", | |
1010 $cals, | |
1011 $where_add | |
1012 ), | |
1013 $id); | |
1014 | |
1015 if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) { | |
1016 $event = $this->_read_postprocess($sql_arr); | |
1017 | |
1018 // also load recurrence exceptions | |
1019 if (!empty($event['recurrence']) && $full) { | |
1020 $event['recurrence']['EXCEPTIONS'] = array_values($this->_load_exceptions($event)); | |
1021 } | |
1022 | |
1023 $this->cache[$id] = $event; | |
1024 return $this->cache[$id]; | |
1025 } | |
1026 | |
1027 return false; | |
1028 } | |
1029 | |
1030 /** | |
1031 * Get event data | |
1032 * | |
1033 * @see calendar_driver::load_events() | |
1034 */ | |
1035 public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null) | |
1036 { | |
1037 if (empty($calendars)) | |
1038 $calendars = array_keys($this->calendars); | |
1039 else if (!is_array($calendars)) | |
1040 $calendars = explode(',', strval($calendars)); | |
1041 | |
1042 // only allow to select from calendars of this use | |
1043 $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars))); | |
1044 | |
1045 // compose (slow) SQL query for searching | |
1046 // FIXME: improve searching using a dedicated col and normalized values | |
1047 if ($query) { | |
1048 foreach (array('title','location','description','categories','attendees') as $col) | |
1049 $sql_query[] = $this->rc->db->ilike($col, '%'.$query.'%'); | |
1050 $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; | |
1051 } | |
1052 | |
1053 if (!$virtual) | |
1054 $sql_add .= ' AND e.recurrence_id = 0'; | |
1055 | |
1056 if ($modifiedsince) | |
1057 $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince)); | |
1058 | |
1059 $events = array(); | |
1060 if (!empty($calendar_ids)) { | |
1061 $result = $this->rc->db->query(sprintf( | |
1062 "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . " | |
1063 WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments | |
1064 FROM " . $this->db_events . " e | |
1065 WHERE e.calendar_id IN (%s) | |
1066 AND e.start <= %s AND e.end >= %s | |
1067 %s", | |
1068 join(',', $calendar_ids), | |
1069 $this->rc->db->fromunixtime($end), | |
1070 $this->rc->db->fromunixtime($start), | |
1071 $sql_add | |
1072 )); | |
1073 | |
1074 while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) { | |
1075 $event = $this->_read_postprocess($sql_arr); | |
1076 $add = true; | |
1077 | |
1078 if (!empty($event['recurrence']) && !$event['recurrence_id']) { | |
1079 // load recurrence exceptions (i.e. for export) | |
1080 if (!$virtual) { | |
1081 $event['recurrence']['EXCEPTIONS'] = $this->_load_exceptions($event); | |
1082 } | |
1083 // check for exception on first instance | |
1084 else { | |
1085 $instance = libcalendaring::recurrence_instance_identifier($event); | |
1086 $exceptions = $this->_load_exceptions($event, $instance); | |
1087 if ($exceptions && is_array($exceptions[$instance])) { | |
1088 $event = $exceptions[$instance]; | |
1089 $add = false; | |
1090 } | |
1091 } | |
1092 } | |
1093 | |
1094 if ($add) | |
1095 $events[] = $event; | |
1096 } | |
1097 } | |
1098 | |
1099 // add events from the address books birthday calendar | |
1100 if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars) && empty($query)) { | |
1101 $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince)); | |
1102 } | |
1103 | |
1104 return $events; | |
1105 } | |
1106 | |
1107 /** | |
1108 * Get number of events in the given calendar | |
1109 * | |
1110 * @param mixed List of calendar IDs to count events (either as array or comma-separated string) | |
1111 * @param integer Date range start (unix timestamp) | |
1112 * @param integer Date range end (unix timestamp) | |
1113 * @return array Hash array with counts grouped by calendar ID | |
1114 */ | |
1115 public function count_events($calendars, $start, $end = null) | |
1116 { | |
1117 // not implemented | |
1118 return array(); | |
1119 } | |
1120 | |
1121 /** | |
1122 * Convert sql record into a rcube style event object | |
1123 */ | |
1124 private function _read_postprocess($event) | |
1125 { | |
1126 $free_busy_map = array_flip($this->free_busy_map); | |
1127 $sensitivity_map = array_flip($this->sensitivity_map); | |
1128 | |
1129 $event['id'] = $event['event_id']; | |
1130 $event['start'] = new DateTime($event['start']); | |
1131 $event['end'] = new DateTime($event['end']); | |
1132 $event['allday'] = intval($event['all_day']); | |
1133 $event['created'] = new DateTime($event['created']); | |
1134 $event['changed'] = new DateTime($event['changed']); | |
1135 $event['free_busy'] = $free_busy_map[$event['free_busy']]; | |
1136 $event['sensitivity'] = $sensitivity_map[$event['sensitivity']]; | |
1137 $event['calendar'] = $event['calendar_id']; | |
1138 $event['recurrence_id'] = intval($event['recurrence_id']); | |
1139 $event['isexception'] = intval($event['isexception']); | |
1140 | |
1141 // parse recurrence rule | |
1142 if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) { | |
1143 $event['recurrence'] = array(); | |
1144 foreach ($m as $rr) { | |
1145 if (is_numeric($rr[2])) | |
1146 $rr[2] = intval($rr[2]); | |
1147 else if ($rr[1] == 'UNTIL') | |
1148 $rr[2] = date_create($rr[2]); | |
1149 else if ($rr[1] == 'RDATE') | |
1150 $rr[2] = array_map('date_create', explode(',', $rr[2])); | |
1151 else if ($rr[1] == 'EXDATE') | |
1152 $rr[2] = array_map('date_create', explode(',', $rr[2])); | |
1153 $event['recurrence'][$rr[1]] = $rr[2]; | |
1154 } | |
1155 } | |
1156 | |
1157 if ($event['recurrence_id']) { | |
1158 libcalendaring::identify_recurrence_instance($event); | |
1159 } | |
1160 | |
1161 if (strlen($event['instance'])) { | |
1162 $event['_instance'] = $event['instance']; | |
1163 | |
1164 if (empty($event['recurrence_id'])) { | |
1165 $event['recurrence_date'] = rcube_utils::anytodatetime($event['_instance'], $event['start']->getTimezone()); | |
1166 } | |
1167 } | |
1168 | |
1169 if ($event['_attachments'] > 0) { | |
1170 $event['attachments'] = (array)$this->list_attachments($event); | |
1171 } | |
1172 | |
1173 // decode serialized event attendees | |
1174 if (strlen($event['attendees'])) { | |
1175 $event['attendees'] = $this->unserialize_attendees($event['attendees']); | |
1176 } | |
1177 else { | |
1178 $event['attendees'] = array(); | |
1179 } | |
1180 | |
1181 // decode serialized alarms | |
1182 if ($event['alarms']) { | |
1183 $event['valarms'] = $this->unserialize_alarms($event['alarms']); | |
1184 } | |
1185 | |
1186 unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['instance'], $event['_attachments']); | |
1187 return $event; | |
1188 } | |
1189 | |
1190 /** | |
1191 * Get a list of pending alarms to be displayed to the user | |
1192 * | |
1193 * @see calendar_driver::pending_alarms() | |
1194 */ | |
1195 public function pending_alarms($time, $calendars = null) | |
1196 { | |
1197 if (empty($calendars)) | |
1198 $calendars = array_keys($this->calendars); | |
1199 else if (is_string($calendars)) | |
1200 $calendars = explode(',', $calendars); | |
1201 | |
1202 // only allow to select from calendars with activated alarms | |
1203 $calendar_ids = array(); | |
1204 foreach ($calendars as $cid) { | |
1205 if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms']) | |
1206 $calendar_ids[] = $cid; | |
1207 } | |
1208 $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids); | |
1209 | |
1210 $alarms = array(); | |
1211 if (!empty($calendar_ids)) { | |
1212 $result = $this->rc->db->query(sprintf( | |
1213 "SELECT * FROM " . $this->db_events . " | |
1214 WHERE calendar_id IN (%s) | |
1215 AND notifyat <= %s AND %s > %s", | |
1216 join(',', $calendar_ids), | |
1217 $this->rc->db->fromunixtime($time), | |
1218 $this->rc->db->quote_identifier('end'), | |
1219 $this->rc->db->fromunixtime($time) | |
1220 )); | |
1221 | |
1222 while ($result && ($event = $this->rc->db->fetch_assoc($result))) | |
1223 $alarms[] = $this->_read_postprocess($event); | |
1224 } | |
1225 | |
1226 return $alarms; | |
1227 } | |
1228 | |
1229 /** | |
1230 * Feedback after showing/sending an alarm notification | |
1231 * | |
1232 * @see calendar_driver::dismiss_alarm() | |
1233 */ | |
1234 public function dismiss_alarm($event_id, $snooze = 0) | |
1235 { | |
1236 // set new notifyat time or unset if not snoozed | |
1237 $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null; | |
1238 | |
1239 $query = $this->rc->db->query(sprintf( | |
1240 "UPDATE " . $this->db_events . " | |
1241 SET changed=%s, notifyat=? | |
1242 WHERE event_id=? | |
1243 AND calendar_id IN (" . $this->calendar_ids . ")", | |
1244 $this->rc->db->now()), | |
1245 $notify_at, | |
1246 $event_id | |
1247 ); | |
1248 | |
1249 return $this->rc->db->affected_rows($query); | |
1250 } | |
1251 | |
1252 /** | |
1253 * Save an attachment related to the given event | |
1254 */ | |
1255 private function add_attachment($attachment, $event_id) | |
1256 { | |
1257 $data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']); | |
1258 | |
1259 $query = $this->rc->db->query( | |
1260 "INSERT INTO " . $this->db_attachments . | |
1261 " (event_id, filename, mimetype, size, data)" . | |
1262 " VALUES (?, ?, ?, ?, ?)", | |
1263 $event_id, | |
1264 $attachment['name'], | |
1265 $attachment['mimetype'], | |
1266 strlen($data), | |
1267 base64_encode($data) | |
1268 ); | |
1269 | |
1270 return $this->rc->db->affected_rows($query); | |
1271 } | |
1272 | |
1273 /** | |
1274 * Remove a specific attachment from the given event | |
1275 */ | |
1276 private function remove_attachment($attachment_id, $event_id) | |
1277 { | |
1278 $query = $this->rc->db->query( | |
1279 "DELETE FROM " . $this->db_attachments . | |
1280 " WHERE attachment_id = ?" . | |
1281 " AND event_id IN (SELECT event_id FROM " . $this->db_events . | |
1282 " WHERE event_id = ?" . | |
1283 " AND calendar_id IN (" . $this->calendar_ids . "))", | |
1284 $attachment_id, | |
1285 $event_id | |
1286 ); | |
1287 | |
1288 return $this->rc->db->affected_rows($query); | |
1289 } | |
1290 | |
1291 /** | |
1292 * List attachments of specified event | |
1293 */ | |
1294 public function list_attachments($event) | |
1295 { | |
1296 $attachments = array(); | |
1297 | |
1298 if (!empty($this->calendar_ids)) { | |
1299 $result = $this->rc->db->query( | |
1300 "SELECT attachment_id AS id, filename AS name, mimetype, size " . | |
1301 " FROM " . $this->db_attachments . | |
1302 " WHERE event_id IN (SELECT event_id FROM " . $this->db_events . | |
1303 " WHERE event_id=?" . | |
1304 " AND calendar_id IN (" . $this->calendar_ids . "))". | |
1305 " ORDER BY filename", | |
1306 $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id'] | |
1307 ); | |
1308 | |
1309 while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { | |
1310 $attachments[] = $arr; | |
1311 } | |
1312 } | |
1313 | |
1314 return $attachments; | |
1315 } | |
1316 | |
1317 /** | |
1318 * Get attachment properties | |
1319 */ | |
1320 public function get_attachment($id, $event) | |
1321 { | |
1322 if (!empty($this->calendar_ids)) { | |
1323 $result = $this->rc->db->query( | |
1324 "SELECT attachment_id AS id, filename AS name, mimetype, size " . | |
1325 " FROM " . $this->db_attachments . | |
1326 " WHERE attachment_id=?". | |
1327 " AND event_id=?", | |
1328 $id, | |
1329 $event['recurrence_id'] ? $event['recurrence_id'] : $event['id'] | |
1330 ); | |
1331 | |
1332 if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { | |
1333 return $arr; | |
1334 } | |
1335 } | |
1336 | |
1337 return null; | |
1338 } | |
1339 | |
1340 /** | |
1341 * Get attachment body | |
1342 */ | |
1343 public function get_attachment_body($id, $event) | |
1344 { | |
1345 if (!empty($this->calendar_ids)) { | |
1346 $result = $this->rc->db->query( | |
1347 "SELECT data " . | |
1348 " FROM " . $this->db_attachments . | |
1349 " WHERE attachment_id=?". | |
1350 " AND event_id=?", | |
1351 $id, | |
1352 $event['id'] | |
1353 ); | |
1354 | |
1355 if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { | |
1356 return base64_decode($arr['data']); | |
1357 } | |
1358 } | |
1359 | |
1360 return null; | |
1361 } | |
1362 | |
1363 /** | |
1364 * Remove the given category | |
1365 */ | |
1366 public function remove_category($name) | |
1367 { | |
1368 $query = $this->rc->db->query( | |
1369 "UPDATE " . $this->db_events . " | |
1370 SET categories='' | |
1371 WHERE categories=? | |
1372 AND calendar_id IN (" . $this->calendar_ids . ")", | |
1373 $name | |
1374 ); | |
1375 | |
1376 return $this->rc->db->affected_rows($query); | |
1377 } | |
1378 | |
1379 /** | |
1380 * Update/replace a category | |
1381 */ | |
1382 public function replace_category($oldname, $name, $color) | |
1383 { | |
1384 $query = $this->rc->db->query( | |
1385 "UPDATE " . $this->db_events . " | |
1386 SET categories=? | |
1387 WHERE categories=? | |
1388 AND calendar_id IN (" . $this->calendar_ids . ")", | |
1389 $name, | |
1390 $oldname | |
1391 ); | |
1392 | |
1393 return $this->rc->db->affected_rows($query); | |
1394 } | |
1395 | |
1396 /** | |
1397 * Helper method to serialize the list of alarms into a string | |
1398 */ | |
1399 private function serialize_alarms($valarms) | |
1400 { | |
1401 foreach ((array)$valarms as $i => $alarm) { | |
1402 if ($alarm['trigger'] instanceof DateTime) { | |
1403 $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c'); | |
1404 } | |
1405 } | |
1406 | |
1407 return $valarms ? json_encode($valarms) : null; | |
1408 } | |
1409 | |
1410 /** | |
1411 * Helper method to decode a serialized list of alarms | |
1412 */ | |
1413 private function unserialize_alarms($alarms) | |
1414 { | |
1415 // decode json serialized alarms | |
1416 if ($alarms && $alarms[0] == '[') { | |
1417 $valarms = json_decode($alarms, true); | |
1418 foreach ($valarms as $i => $alarm) { | |
1419 if ($alarm['trigger'][0] == '@') { | |
1420 try { | |
1421 $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); | |
1422 } | |
1423 catch (Exception $e) { | |
1424 unset($valarms[$i]); | |
1425 } | |
1426 } | |
1427 } | |
1428 } | |
1429 // convert legacy alarms data | |
1430 else if (strlen($alarms)) { | |
1431 list($trigger, $action) = explode(':', $alarms, 2); | |
1432 if ($trigger = libcalendaring::parse_alarm_value($trigger)) { | |
1433 $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); | |
1434 } | |
1435 } | |
1436 | |
1437 return $valarms; | |
1438 } | |
1439 | |
1440 /** | |
1441 * Helper method to decode the attendees list from string | |
1442 */ | |
1443 private function unserialize_attendees($s_attendees) | |
1444 { | |
1445 $attendees = array(); | |
1446 | |
1447 // decode json serialized string | |
1448 if ($s_attendees[0] == '[') { | |
1449 $attendees = json_decode($s_attendees, true); | |
1450 } | |
1451 // decode the old serialization format | |
1452 else { | |
1453 foreach (explode("\n", $s_attendees) as $line) { | |
1454 $att = array(); | |
1455 foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) { | |
1456 list($key, $value) = explode("=", $prop); | |
1457 $att[strtolower($key)] = stripslashes(trim($value, '""')); | |
1458 } | |
1459 $attendees[] = $att; | |
1460 } | |
1461 } | |
1462 | |
1463 return $attendees; | |
1464 } | |
1465 | |
1466 /** | |
1467 * Handler for user_delete plugin hook | |
1468 */ | |
1469 public function user_delete($args) | |
1470 { | |
1471 $db = $this->rc->db; | |
1472 $user = $args['user']; | |
1473 $event_ids = array(); | |
1474 | |
1475 $events = $db->query( | |
1476 "SELECT event_id FROM " . $this->db_events . " AS ev" . | |
1477 " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)". | |
1478 " WHERE user_id=?", | |
1479 $user->ID); | |
1480 | |
1481 while ($row = $db->fetch_assoc($events)) { | |
1482 $event_ids[] = $row['event_id']; | |
1483 } | |
1484 | |
1485 if (!empty($event_ids)) { | |
1486 foreach (array($this->db_attachments, $this->db_events) as $table) { | |
1487 $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids))); | |
1488 } | |
1489 } | |
1490 | |
1491 foreach (array($this->db_calendars, 'itipinvitations') as $table) { | |
1492 $db->query("DELETE FROM $table WHERE user_id=?", $user->ID); | |
1493 } | |
1494 } | |
1495 | |
1496 } |