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