Mercurial > hg > rc1
comparison plugins/calendar/drivers/kolab/kolab_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 * Kolab driver for the Calendar plugin | |
| 5 * | |
| 6 * @version @package_version@ | |
| 7 * @author Thomas Bruederli <bruederli@kolabsys.com> | |
| 8 * @author Aleksander Machniak <machniak@kolabsys.com> | |
| 9 * | |
| 10 * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com> | |
| 11 * | |
| 12 * This program is free software: you can redistribute it and/or modify | |
| 13 * it under the terms of the GNU Affero General Public License as | |
| 14 * published by the Free Software Foundation, either version 3 of the | |
| 15 * License, or (at your option) any later version. | |
| 16 * | |
| 17 * This program is distributed in the hope that it will be useful, | |
| 18 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 20 * GNU Affero General Public License for more details. | |
| 21 * | |
| 22 * You should have received a copy of the GNU Affero General Public License | |
| 23 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| 24 */ | |
| 25 | |
| 26 class kolab_driver extends calendar_driver | |
| 27 { | |
| 28 const INVITATIONS_CALENDAR_PENDING = '--invitation--pending'; | |
| 29 const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined'; | |
| 30 | |
| 31 // features this backend supports | |
| 32 public $alarms = true; | |
| 33 public $attendees = true; | |
| 34 public $freebusy = true; | |
| 35 public $attachments = true; | |
| 36 public $undelete = true; | |
| 37 public $alarm_types = array('DISPLAY','AUDIO'); | |
| 38 public $categoriesimmutable = true; | |
| 39 | |
| 40 private $rc; | |
| 41 private $cal; | |
| 42 private $calendars; | |
| 43 private $has_writeable = false; | |
| 44 private $freebusy_trigger = false; | |
| 45 private $bonnie_api = false; | |
| 46 | |
| 47 /** | |
| 48 * Default constructor | |
| 49 */ | |
| 50 public function __construct($cal) | |
| 51 { | |
| 52 $cal->require_plugin('libkolab'); | |
| 53 | |
| 54 // load helper classes *after* libkolab has been loaded (#3248) | |
| 55 require_once(dirname(__FILE__) . '/kolab_calendar.php'); | |
| 56 require_once(dirname(__FILE__) . '/kolab_user_calendar.php'); | |
| 57 require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php'); | |
| 58 | |
| 59 $this->cal = $cal; | |
| 60 $this->rc = $cal->rc; | |
| 61 | |
| 62 $this->cal->register_action('push-freebusy', array($this, 'push_freebusy')); | |
| 63 $this->cal->register_action('calendar-acl', array($this, 'calendar_acl')); | |
| 64 | |
| 65 $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false); | |
| 66 | |
| 67 if (kolab_storage::$version == '2.0') { | |
| 68 $this->alarm_types = array('DISPLAY'); | |
| 69 $this->alarm_absolute = false; | |
| 70 } | |
| 71 | |
| 72 // get configuration for the Bonnie API | |
| 73 $this->bonnie_api = libkolab::get_bonnie_api(); | |
| 74 | |
| 75 // calendar uses fully encoded identifiers | |
| 76 kolab_storage::$encode_ids = true; | |
| 77 } | |
| 78 | |
| 79 | |
| 80 /** | |
| 81 * Read available calendars from server | |
| 82 */ | |
| 83 private function _read_calendars() | |
| 84 { | |
| 85 // already read sources | |
| 86 if (isset($this->calendars)) | |
| 87 return $this->calendars; | |
| 88 | |
| 89 // get all folders that have "event" type, sorted by namespace/name | |
| 90 $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true)); | |
| 91 | |
| 92 $this->calendars = array(); | |
| 93 foreach ($folders as $folder) { | |
| 94 if ($folder instanceof kolab_storage_folder_user) { | |
| 95 $calendar = new kolab_user_calendar($folder, $this->cal); | |
| 96 $calendar->subscriptions = count($folder->children) > 0; | |
| 97 } | |
| 98 else { | |
| 99 $calendar = new kolab_calendar($folder->name, $this->cal); | |
| 100 } | |
| 101 | |
| 102 if ($calendar->ready) { | |
| 103 $this->calendars[$calendar->id] = $calendar; | |
| 104 if ($calendar->editable) | |
| 105 $this->has_writeable = true; | |
| 106 } | |
| 107 } | |
| 108 | |
| 109 return $this->calendars; | |
| 110 } | |
| 111 | |
| 112 /** | |
| 113 * Get a list of available calendars from this source | |
| 114 * | |
| 115 * @param integer $filter Bitmask defining filter criterias | |
| 116 * @param object $tree Reference to hierarchical folder tree object | |
| 117 * | |
| 118 * @return array List of calendars | |
| 119 */ | |
| 120 public function list_calendars($filter = 0, &$tree = null) | |
| 121 { | |
| 122 $this->_read_calendars(); | |
| 123 | |
| 124 // attempt to create a default calendar for this user | |
| 125 if (!$this->has_writeable) { | |
| 126 if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) { | |
| 127 unset($this->calendars); | |
| 128 $this->_read_calendars(); | |
| 129 } | |
| 130 } | |
| 131 | |
| 132 $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); | |
| 133 $folders = $this->filter_calendars($filter); | |
| 134 $calendars = array(); | |
| 135 | |
| 136 // include virtual folders for a full folder tree | |
| 137 if (!is_null($tree)) | |
| 138 $folders = kolab_storage::folder_hierarchy($folders, $tree); | |
| 139 | |
| 140 foreach ($folders as $id => $cal) { | |
| 141 $fullname = $cal->get_name(); | |
| 142 $listname = $cal->get_foldername(); | |
| 143 $imap_path = explode($delim, $cal->name); | |
| 144 | |
| 145 // find parent | |
| 146 do { | |
| 147 array_pop($imap_path); | |
| 148 $parent_id = kolab_storage::folder_id(join($delim, $imap_path)); | |
| 149 } | |
| 150 while (count($imap_path) > 1 && !$this->calendars[$parent_id]); | |
| 151 | |
| 152 // restore "real" parent ID | |
| 153 if ($parent_id && !$this->calendars[$parent_id]) { | |
| 154 $parent_id = kolab_storage::folder_id($cal->get_parent()); | |
| 155 } | |
| 156 | |
| 157 // turn a kolab_storage_folder object into a kolab_calendar | |
| 158 if ($cal instanceof kolab_storage_folder) { | |
| 159 $cal = new kolab_calendar($cal->name, $this->cal); | |
| 160 $this->calendars[$cal->id] = $cal; | |
| 161 } | |
| 162 | |
| 163 // special handling for user or virtual folders | |
| 164 if ($cal instanceof kolab_storage_folder_user) { | |
| 165 $calendars[$cal->id] = array( | |
| 166 'id' => $cal->id, | |
| 167 'name' => $fullname, | |
| 168 'listname' => $listname, | |
| 169 'editname' => $cal->get_foldername(), | |
| 170 'color' => $cal->get_color(), | |
| 171 'active' => $cal->is_active(), | |
| 172 'title' => $cal->get_owner(), | |
| 173 'owner' => $cal->get_owner(), | |
| 174 'history' => false, | |
| 175 'virtual' => false, | |
| 176 'editable' => false, | |
| 177 'group' => 'other', | |
| 178 'class' => 'user', | |
| 179 'removable' => true, | |
| 180 ); | |
| 181 } | |
| 182 else if ($cal->virtual) { | |
| 183 $calendars[$cal->id] = array( | |
| 184 'id' => $cal->id, | |
| 185 'name' => $fullname, | |
| 186 'listname' => $listname, | |
| 187 'editname' => $cal->get_foldername(), | |
| 188 'virtual' => true, | |
| 189 'editable' => false, | |
| 190 'group' => $cal->get_namespace(), | |
| 191 'class' => 'folder', | |
| 192 ); | |
| 193 } | |
| 194 else { | |
| 195 $calendars[$cal->id] = array( | |
| 196 'id' => $cal->id, | |
| 197 'name' => $fullname, | |
| 198 'listname' => $listname, | |
| 199 'editname' => $cal->get_foldername(), | |
| 200 'title' => $cal->get_title(), | |
| 201 'color' => $cal->get_color(), | |
| 202 'editable' => $cal->editable, | |
| 203 'rights' => $cal->rights, | |
| 204 'showalarms' => $cal->alarms, | |
| 205 'history' => !empty($this->bonnie_api), | |
| 206 'group' => $cal->get_namespace(), | |
| 207 'default' => $cal->default, | |
| 208 'active' => $cal->is_active(), | |
| 209 'owner' => $cal->get_owner(), | |
| 210 'children' => true, // TODO: determine if that folder indeed has child folders | |
| 211 'parent' => $parent_id, | |
| 212 'subtype' => $cal->subtype, | |
| 213 'caldavurl' => $cal->get_caldav_url(), | |
| 214 'removable' => !$cal->default, | |
| 215 ); | |
| 216 } | |
| 217 | |
| 218 if ($cal->subscriptions) { | |
| 219 $calendars[$cal->id]['subscribed'] = $cal->is_subscribed(); | |
| 220 } | |
| 221 } | |
| 222 | |
| 223 // list virtual calendars showing invitations | |
| 224 if ($this->rc->config->get('kolab_invitation_calendars')) { | |
| 225 foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) { | |
| 226 $cal = new kolab_invitation_calendar($id, $this->cal); | |
| 227 $this->calendars[$cal->id] = $cal; | |
| 228 if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) { | |
| 229 $calendars[$id] = array( | |
| 230 'id' => $cal->id, | |
| 231 'name' => $cal->get_name(), | |
| 232 'listname' => $cal->get_name(), | |
| 233 'editname' => $cal->get_foldername(), | |
| 234 'title' => $cal->get_title(), | |
| 235 'color' => $cal->get_color(), | |
| 236 'editable' => $cal->editable, | |
| 237 'rights' => $cal->rights, | |
| 238 'showalarms' => $cal->alarms, | |
| 239 'history' => !empty($this->bonnie_api), | |
| 240 'group' => 'x-invitations', | |
| 241 'default' => false, | |
| 242 'active' => $cal->is_active(), | |
| 243 'owner' => $cal->get_owner(), | |
| 244 'children' => false, | |
| 245 ); | |
| 246 | |
| 247 if ($id == self::INVITATIONS_CALENDAR_PENDING) { | |
| 248 $calendars[$id]['counts'] = true; | |
| 249 } | |
| 250 | |
| 251 if (is_object($tree)) { | |
| 252 $tree->children[] = $cal; | |
| 253 } | |
| 254 } | |
| 255 } | |
| 256 } | |
| 257 | |
| 258 // append the virtual birthdays calendar | |
| 259 if ($this->rc->config->get('calendar_contact_birthdays', false)) { | |
| 260 $id = self::BIRTHDAY_CALENDAR_ID; | |
| 261 $prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs | |
| 262 if (!($filter & self::FILTER_ACTIVE) || $prefs[$id]['active']) { | |
| 263 $calendars[$id] = array( | |
| 264 'id' => $id, | |
| 265 'name' => $this->cal->gettext('birthdays'), | |
| 266 'listname' => $this->cal->gettext('birthdays'), | |
| 267 'color' => $prefs[$id]['color'] ?: '87CEFA', | |
| 268 'active' => (bool)$prefs[$id]['active'], | |
| 269 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'), | |
| 270 'group' => 'x-birthdays', | |
| 271 'editable' => false, | |
| 272 'default' => false, | |
| 273 'children' => false, | |
| 274 'history' => false, | |
| 275 ); | |
| 276 } | |
| 277 } | |
| 278 | |
| 279 return $calendars; | |
| 280 } | |
| 281 | |
| 282 /** | |
| 283 * Get list of calendars according to specified filters | |
| 284 * | |
| 285 * @param integer Bitmask defining restrictions. See FILTER_* constants for possible values. | |
| 286 * | |
| 287 * @return array List of calendars | |
| 288 */ | |
| 289 protected function filter_calendars($filter) | |
| 290 { | |
| 291 $this->_read_calendars(); | |
| 292 | |
| 293 $calendars = array(); | |
| 294 | |
| 295 $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array( | |
| 296 'list' => $this->calendars, | |
| 297 'calendars' => $calendars, | |
| 298 'filter' => $filter, | |
| 299 )); | |
| 300 | |
| 301 if ($plugin['abort']) { | |
| 302 return $plugin['calendars']; | |
| 303 } | |
| 304 | |
| 305 $personal = $filter & self::FILTER_PERSONAL; | |
| 306 $shared = $filter & self::FILTER_SHARED; | |
| 307 | |
| 308 foreach ($this->calendars as $cal) { | |
| 309 if (!$cal->ready) { | |
| 310 continue; | |
| 311 } | |
| 312 if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) { | |
| 313 continue; | |
| 314 } | |
| 315 if (($filter & self::FILTER_INSERTABLE) && !$cal->insert) { | |
| 316 continue; | |
| 317 } | |
| 318 if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) { | |
| 319 continue; | |
| 320 } | |
| 321 if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') { | |
| 322 continue; | |
| 323 } | |
| 324 if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') { | |
| 325 continue; | |
| 326 } | |
| 327 if ($personal || $shared) { | |
| 328 $ns = $cal->get_namespace(); | |
| 329 if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) { | |
| 330 continue; | |
| 331 } | |
| 332 } | |
| 333 | |
| 334 $calendars[$cal->id] = $cal; | |
| 335 } | |
| 336 | |
| 337 return $calendars; | |
| 338 } | |
| 339 | |
| 340 | |
| 341 /** | |
| 342 * Get the kolab_calendar instance for the given calendar ID | |
| 343 * | |
| 344 * @param string Calendar identifier (encoded imap folder name) | |
| 345 * @return object kolab_calendar Object nor null if calendar doesn't exist | |
| 346 */ | |
| 347 public function get_calendar($id) | |
| 348 { | |
| 349 $this->_read_calendars(); | |
| 350 | |
| 351 // create calendar object if necesary | |
| 352 if (!$this->calendars[$id]) { | |
| 353 if (in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) { | |
| 354 $this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal); | |
| 355 } | |
| 356 else if ($id !== self::BIRTHDAY_CALENDAR_ID) { | |
| 357 $calendar = kolab_calendar::factory($id, $this->cal); | |
| 358 if ($calendar->ready) { | |
| 359 $this->calendars[$calendar->id] = $calendar; | |
| 360 } | |
| 361 } | |
| 362 } | |
| 363 | |
| 364 return $this->calendars[$id]; | |
| 365 } | |
| 366 | |
| 367 /** | |
| 368 * Create a new calendar assigned to the current user | |
| 369 * | |
| 370 * @param array Hash array with calendar properties | |
| 371 * name: Calendar name | |
| 372 * color: The color of the calendar | |
| 373 * @return mixed ID of the calendar on success, False on error | |
| 374 */ | |
| 375 public function create_calendar($prop) | |
| 376 { | |
| 377 $prop['type'] = 'event'; | |
| 378 $prop['active'] = true; | |
| 379 $prop['subscribed'] = true; | |
| 380 $folder = kolab_storage::folder_update($prop); | |
| 381 | |
| 382 if ($folder === false) { | |
| 383 $this->last_error = $this->cal->gettext(kolab_storage::$last_error); | |
| 384 return false; | |
| 385 } | |
| 386 | |
| 387 // create ID | |
| 388 $id = kolab_storage::folder_id($folder); | |
| 389 | |
| 390 // save color in user prefs (temp. solution) | |
| 391 $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); | |
| 392 | |
| 393 if (isset($prop['color'])) | |
| 394 $prefs['kolab_calendars'][$id]['color'] = $prop['color']; | |
| 395 if (isset($prop['showalarms'])) | |
| 396 $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; | |
| 397 | |
| 398 if ($prefs['kolab_calendars'][$id]) | |
| 399 $this->rc->user->save_prefs($prefs); | |
| 400 | |
| 401 return $id; | |
| 402 } | |
| 403 | |
| 404 | |
| 405 /** | |
| 406 * Update properties of an existing calendar | |
| 407 * | |
| 408 * @see calendar_driver::edit_calendar() | |
| 409 */ | |
| 410 public function edit_calendar($prop) | |
| 411 { | |
| 412 if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) { | |
| 413 $id = $cal->update($prop); | |
| 414 } | |
| 415 else { | |
| 416 $id = $prop['id']; | |
| 417 } | |
| 418 | |
| 419 // fallback to local prefs | |
| 420 $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); | |
| 421 unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']); | |
| 422 | |
| 423 if (isset($prop['color'])) | |
| 424 $prefs['kolab_calendars'][$id]['color'] = $prop['color']; | |
| 425 | |
| 426 if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID) | |
| 427 $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : ''; | |
| 428 else if (isset($prop['showalarms'])) | |
| 429 $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; | |
| 430 | |
| 431 if (!empty($prefs['kolab_calendars'][$id])) | |
| 432 $this->rc->user->save_prefs($prefs); | |
| 433 | |
| 434 return true; | |
| 435 } | |
| 436 | |
| 437 | |
| 438 /** | |
| 439 * Set active/subscribed state of a calendar | |
| 440 * | |
| 441 * @see calendar_driver::subscribe_calendar() | |
| 442 */ | |
| 443 public function subscribe_calendar($prop) | |
| 444 { | |
| 445 if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) { | |
| 446 $ret = false; | |
| 447 if (isset($prop['permanent'])) | |
| 448 $ret |= $cal->storage->subscribe(intval($prop['permanent'])); | |
| 449 if (isset($prop['active'])) | |
| 450 $ret |= $cal->storage->activate(intval($prop['active'])); | |
| 451 | |
| 452 // apply to child folders, too | |
| 453 if ($prop['recursive']) { | |
| 454 foreach ((array)kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) { | |
| 455 if (isset($prop['permanent'])) | |
| 456 ($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder)); | |
| 457 if (isset($prop['active'])) | |
| 458 ($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder)); | |
| 459 } | |
| 460 } | |
| 461 return $ret; | |
| 462 } | |
| 463 else { | |
| 464 // save state in local prefs | |
| 465 $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); | |
| 466 $prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active']; | |
| 467 $this->rc->user->save_prefs($prefs); | |
| 468 return true; | |
| 469 } | |
| 470 | |
| 471 return false; | |
| 472 } | |
| 473 | |
| 474 | |
| 475 /** | |
| 476 * Delete the given calendar with all its contents | |
| 477 * | |
| 478 * @see calendar_driver::delete_calendar() | |
| 479 */ | |
| 480 public function delete_calendar($prop) | |
| 481 { | |
| 482 if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) { | |
| 483 $folder = $cal->get_realname(); | |
| 484 // TODO: unsubscribe if no admin rights | |
| 485 if (kolab_storage::folder_delete($folder)) { | |
| 486 // remove color in user prefs (temp. solution) | |
| 487 $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); | |
| 488 unset($prefs['kolab_calendars'][$prop['id']]); | |
| 489 | |
| 490 $this->rc->user->save_prefs($prefs); | |
| 491 return true; | |
| 492 } | |
| 493 else | |
| 494 $this->last_error = kolab_storage::$last_error; | |
| 495 } | |
| 496 | |
| 497 return false; | |
| 498 } | |
| 499 | |
| 500 | |
| 501 /** | |
| 502 * Search for shared or otherwise not listed calendars the user has access | |
| 503 * | |
| 504 * @param string Search string | |
| 505 * @param string Section/source to search | |
| 506 * @return array List of calendars | |
| 507 */ | |
| 508 public function search_calendars($query, $source) | |
| 509 { | |
| 510 if (!kolab_storage::setup()) | |
| 511 return array(); | |
| 512 | |
| 513 $this->calendars = array(); | |
| 514 $this->search_more_results = false; | |
| 515 | |
| 516 // find unsubscribed IMAP folders that have "event" type | |
| 517 if ($source == 'folders') { | |
| 518 foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) { | |
| 519 $calendar = new kolab_calendar($folder->name, $this->cal); | |
| 520 $this->calendars[$calendar->id] = $calendar; | |
| 521 } | |
| 522 } | |
| 523 // find other user's virtual calendars | |
| 524 else if ($source == 'users') { | |
| 525 $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number | |
| 526 foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) { | |
| 527 $calendar = new kolab_user_calendar($user, $this->cal); | |
| 528 $this->calendars[$calendar->id] = $calendar; | |
| 529 | |
| 530 // search for calendar folders shared by this user | |
| 531 foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) { | |
| 532 $cal = new kolab_calendar($foldername, $this->cal); | |
| 533 $this->calendars[$cal->id] = $cal; | |
| 534 $calendar->subscriptions = true; | |
| 535 } | |
| 536 } | |
| 537 | |
| 538 if ($count > $limit) { | |
| 539 $this->search_more_results = true; | |
| 540 } | |
| 541 } | |
| 542 | |
| 543 // don't list the birthday calendar | |
| 544 $this->rc->config->set('calendar_contact_birthdays', false); | |
| 545 $this->rc->config->set('kolab_invitation_calendars', false); | |
| 546 | |
| 547 return $this->list_calendars(); | |
| 548 } | |
| 549 | |
| 550 | |
| 551 /** | |
| 552 * Fetch a single event | |
| 553 * | |
| 554 * @see calendar_driver::get_event() | |
| 555 * @return array Hash array with event properties, false if not found | |
| 556 */ | |
| 557 public function get_event($event, $scope = 0, $full = false) | |
| 558 { | |
| 559 if (is_array($event)) { | |
| 560 $id = $event['id'] ?: $event['uid']; | |
| 561 $cal = $event['calendar']; | |
| 562 | |
| 563 // we're looking for a recurring instance: expand the ID to our internal convention for recurring instances | |
| 564 if (!$event['id'] && $event['_instance']) { | |
| 565 $id .= '-' . $event['_instance']; | |
| 566 } | |
| 567 } | |
| 568 else { | |
| 569 $id = $event; | |
| 570 } | |
| 571 | |
| 572 if ($cal) { | |
| 573 if ($storage = $this->get_calendar($cal)) { | |
| 574 $result = $storage->get_event($id); | |
| 575 return self::to_rcube_event($result); | |
| 576 } | |
| 577 // get event from the address books birthday calendar | |
| 578 else if ($cal == self::BIRTHDAY_CALENDAR_ID) { | |
| 579 return $this->get_birthday_event($id); | |
| 580 } | |
| 581 } | |
| 582 // iterate over all calendar folders and search for the event ID | |
| 583 else { | |
| 584 foreach ($this->filter_calendars($scope) as $calendar) { | |
| 585 if ($result = $calendar->get_event($id)) { | |
| 586 return self::to_rcube_event($result); | |
| 587 } | |
| 588 } | |
| 589 } | |
| 590 | |
| 591 return false; | |
| 592 } | |
| 593 | |
| 594 /** | |
| 595 * Add a single event to the database | |
| 596 * | |
| 597 * @see calendar_driver::new_event() | |
| 598 */ | |
| 599 public function new_event($event) | |
| 600 { | |
| 601 if (!$this->validate($event)) | |
| 602 return false; | |
| 603 | |
| 604 $event = self::from_rcube_event($event); | |
| 605 | |
| 606 if (!$event['calendar']) { | |
| 607 $this->_read_calendars(); | |
| 608 $event['calendar'] = reset(array_keys($this->calendars)); | |
| 609 } | |
| 610 | |
| 611 if ($storage = $this->get_calendar($event['calendar'])) { | |
| 612 // if this is a recurrence instance, append as exception to an already existing object for this UID | |
| 613 if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) { | |
| 614 self::add_exception($master, $event); | |
| 615 $success = $storage->update_event($master); | |
| 616 } | |
| 617 else { | |
| 618 $success = $storage->insert_event($event); | |
| 619 } | |
| 620 | |
| 621 if ($success && $this->freebusy_trigger) { | |
| 622 $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); | |
| 623 $this->freebusy_trigger = false; // disable after first execution (#2355) | |
| 624 } | |
| 625 | |
| 626 return $success; | |
| 627 } | |
| 628 | |
| 629 return false; | |
| 630 } | |
| 631 | |
| 632 /** | |
| 633 * Update an event entry with the given data | |
| 634 * | |
| 635 * @see calendar_driver::new_event() | |
| 636 * @return boolean True on success, False on error | |
| 637 */ | |
| 638 public function edit_event($event) | |
| 639 { | |
| 640 if (!($storage = $this->get_calendar($event['calendar']))) | |
| 641 return false; | |
| 642 | |
| 643 return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id']))); | |
| 644 } | |
| 645 | |
| 646 /** | |
| 647 * Extended event editing with possible changes to the argument | |
| 648 * | |
| 649 * @param array Hash array with event properties | |
| 650 * @param string New participant status | |
| 651 * @param array List of hash arrays with updated attendees | |
| 652 * @return boolean True on success, False on error | |
| 653 */ | |
| 654 public function edit_rsvp(&$event, $status, $attendees) | |
| 655 { | |
| 656 $update_event = $event; | |
| 657 | |
| 658 // apply changes to master (and all exceptions) | |
| 659 if ($event['_savemode'] == 'all' && $event['recurrence_id']) { | |
| 660 if ($storage = $this->get_calendar($event['calendar'])) { | |
| 661 $update_event = $storage->get_event($event['recurrence_id']); | |
| 662 $update_event['_savemode'] = $event['_savemode']; | |
| 663 $update_event['id'] = $update_event['uid']; | |
| 664 unset($update_event['recurrence_id']); | |
| 665 calendar::merge_attendee_data($update_event, $attendees); | |
| 666 } | |
| 667 } | |
| 668 | |
| 669 if ($ret = $this->update_attendees($update_event, $attendees)) { | |
| 670 // replace with master event (for iTip reply) | |
| 671 $event = self::to_rcube_event($update_event); | |
| 672 | |
| 673 // re-assign to the according (virtual) calendar | |
| 674 if ($this->rc->config->get('kolab_invitation_calendars')) { | |
| 675 if (strtoupper($status) == 'DECLINED') | |
| 676 $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED; | |
| 677 else if (strtoupper($status) == 'NEEDS-ACTION') | |
| 678 $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING; | |
| 679 else if ($event['_folder_id']) | |
| 680 $event['calendar'] = $event['_folder_id']; | |
| 681 } | |
| 682 } | |
| 683 | |
| 684 return $ret; | |
| 685 } | |
| 686 | |
| 687 /** | |
| 688 * Update the participant status for the given attendees | |
| 689 * | |
| 690 * @see calendar_driver::update_attendees() | |
| 691 */ | |
| 692 public function update_attendees(&$event, $attendees) | |
| 693 { | |
| 694 // for this-and-future updates, merge the updated attendees onto all exceptions in range | |
| 695 if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) { | |
| 696 if (!($storage = $this->get_calendar($event['calendar']))) | |
| 697 return false; | |
| 698 | |
| 699 // load master event | |
| 700 $master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event; | |
| 701 | |
| 702 // apply attendee update to each existing exception | |
| 703 if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) { | |
| 704 $saved = false; | |
| 705 foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) { | |
| 706 // merge the new event properties onto future exceptions | |
| 707 if ($exception['_instance'] >= strval($event['_instance'])) { | |
| 708 calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees); | |
| 709 } | |
| 710 // update a specific instance | |
| 711 if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) { | |
| 712 $saved = true; | |
| 713 } | |
| 714 } | |
| 715 | |
| 716 // add the given event as new exception | |
| 717 if (!$saved && $event['id'] != $master['id']) { | |
| 718 $event['thisandfuture'] = true; | |
| 719 $master['recurrence']['EXCEPTIONS'][] = $event; | |
| 720 } | |
| 721 | |
| 722 // set link to top-level exceptions | |
| 723 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS']; | |
| 724 | |
| 725 return $this->update_event($master); | |
| 726 } | |
| 727 } | |
| 728 | |
| 729 // just update the given event (instance) | |
| 730 return $this->update_event($event); | |
| 731 } | |
| 732 | |
| 733 /** | |
| 734 * Move a single event | |
| 735 * | |
| 736 * @see calendar_driver::move_event() | |
| 737 * @return boolean True on success, False on error | |
| 738 */ | |
| 739 public function move_event($event) | |
| 740 { | |
| 741 if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) { | |
| 742 unset($ev['sequence']); | |
| 743 self::clear_attandee_noreply($ev); | |
| 744 return $this->update_event($event + $ev); | |
| 745 } | |
| 746 | |
| 747 return false; | |
| 748 } | |
| 749 | |
| 750 /** | |
| 751 * Resize a single event | |
| 752 * | |
| 753 * @see calendar_driver::resize_event() | |
| 754 * @return boolean True on success, False on error | |
| 755 */ | |
| 756 public function resize_event($event) | |
| 757 { | |
| 758 if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) { | |
| 759 unset($ev['sequence']); | |
| 760 self::clear_attandee_noreply($ev); | |
| 761 return $this->update_event($event + $ev); | |
| 762 } | |
| 763 | |
| 764 return false; | |
| 765 } | |
| 766 | |
| 767 /** | |
| 768 * Remove a single event | |
| 769 * | |
| 770 * @param array Hash array with event properties: | |
| 771 * id: Event identifier | |
| 772 * @param boolean Remove record(s) irreversible (mark as deleted otherwise) | |
| 773 * | |
| 774 * @return boolean True on success, False on error | |
| 775 */ | |
| 776 public function remove_event($event, $force = true) | |
| 777 { | |
| 778 $ret = true; | |
| 779 $success = false; | |
| 780 $savemode = $event['_savemode']; | |
| 781 $decline = $event['_decline']; | |
| 782 | |
| 783 if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) { | |
| 784 $event['_savemode'] = $savemode; | |
| 785 $savemode = 'all'; | |
| 786 $master = $event; | |
| 787 | |
| 788 $this->rc->session->remove('calendar_restore_event_data'); | |
| 789 | |
| 790 // read master if deleting a recurring event | |
| 791 if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) { | |
| 792 $master = $storage->get_event($event['uid']); | |
| 793 $savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all'); | |
| 794 | |
| 795 // force 'current' mode for single occurrences stored as exception | |
| 796 if (!$event['recurrence'] && !$event['recurrence_id'] && $event['isexception']) | |
| 797 $savemode = 'current'; | |
| 798 } | |
| 799 | |
| 800 // removing an exception instance | |
| 801 if (($event['recurrence_id'] || $event['isexception']) && is_array($master['exceptions'])) { | |
| 802 foreach ($master['exceptions'] as $i => $exception) { | |
| 803 if ($exception['_instance'] == $event['_instance']) { | |
| 804 unset($master['exceptions'][$i]); | |
| 805 // set event date back to the actual occurrence | |
| 806 if ($exception['recurrence_date']) | |
| 807 $event['start'] = $exception['recurrence_date']; | |
| 808 } | |
| 809 } | |
| 810 | |
| 811 if (is_array($master['recurrence'])) { | |
| 812 $master['recurrence']['EXCEPTIONS'] = &$master['exceptions']; | |
| 813 } | |
| 814 } | |
| 815 | |
| 816 switch ($savemode) { | |
| 817 case 'current': | |
| 818 $_SESSION['calendar_restore_event_data'] = $master; | |
| 819 | |
| 820 // removing the first instance => just move to next occurence | |
| 821 if ($master['recurrence'] && $event['_instance'] == libcalendaring::recurrence_instance_identifier($master)) { | |
| 822 $recurring = reset($storage->get_recurring_events($event, $event['start'], null, $event['id'].'-1')); | |
| 823 | |
| 824 // no future instances found: delete the master event (bug #1677) | |
| 825 if (!$recurring['start']) { | |
| 826 $success = $storage->delete_event($master, $force); | |
| 827 break; | |
| 828 } | |
| 829 | |
| 830 $master['start'] = $recurring['start']; | |
| 831 $master['end'] = $recurring['end']; | |
| 832 if ($master['recurrence']['COUNT']) | |
| 833 $master['recurrence']['COUNT']--; | |
| 834 } | |
| 835 // remove the matching RDATE entry | |
| 836 else if ($master['recurrence']['RDATE']) { | |
| 837 foreach ($master['recurrence']['RDATE'] as $j => $rdate) { | |
| 838 if ($rdate->format('Ymd') == $event['start']->format('Ymd')) { | |
| 839 unset($master['recurrence']['RDATE'][$j]); | |
| 840 break; | |
| 841 } | |
| 842 } | |
| 843 } | |
| 844 else { // add exception to master event | |
| 845 $master['recurrence']['EXDATE'][] = $event['start']; | |
| 846 } | |
| 847 $success = $storage->update_event($master); | |
| 848 break; | |
| 849 | |
| 850 case 'future': | |
| 851 $master['_instance'] = libcalendaring::recurrence_instance_identifier($master); | |
| 852 if ($master['_instance'] != $event['_instance']) { | |
| 853 $_SESSION['calendar_restore_event_data'] = $master; | |
| 854 | |
| 855 // set until-date on master event | |
| 856 $master['recurrence']['UNTIL'] = clone $event['start']; | |
| 857 $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); | |
| 858 unset($master['recurrence']['COUNT']); | |
| 859 | |
| 860 // if all future instances are deleted, remove recurrence rule entirely (bug #1677) | |
| 861 if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) { | |
| 862 $master['recurrence'] = array(); | |
| 863 } | |
| 864 // remove matching RDATE entries | |
| 865 else if ($master['recurrence']['RDATE']) { | |
| 866 foreach ($master['recurrence']['RDATE'] as $j => $rdate) { | |
| 867 if ($rdate->format('Ymd') == $event['start']->format('Ymd')) { | |
| 868 $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j); | |
| 869 break; | |
| 870 } | |
| 871 } | |
| 872 } | |
| 873 | |
| 874 $success = $storage->update_event($master); | |
| 875 $ret = $master['uid']; | |
| 876 break; | |
| 877 } | |
| 878 | |
| 879 default: // 'all' is default | |
| 880 // removing the master event with loose exceptions (not recurring though) | |
| 881 if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) { | |
| 882 // make the first exception the new master | |
| 883 $newmaster = array_shift($master['exceptions']); | |
| 884 $newmaster['exceptions'] = $master['exceptions']; | |
| 885 $newmaster['_attachments'] = $master['_attachments']; | |
| 886 $newmaster['_mailbox'] = $master['_mailbox']; | |
| 887 $newmaster['_msguid'] = $master['_msguid']; | |
| 888 | |
| 889 $success = $storage->update_event($newmaster); | |
| 890 } | |
| 891 else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) { | |
| 892 // don't delete but set PARTSTAT=DECLINED | |
| 893 if ($this->cal->lib->set_partstat($master, 'DECLINED')) { | |
| 894 $success = $storage->update_event($master); | |
| 895 } | |
| 896 } | |
| 897 | |
| 898 if (!$success) | |
| 899 $success = $storage->delete_event($master, $force); | |
| 900 break; | |
| 901 } | |
| 902 } | |
| 903 | |
| 904 if ($success && $this->freebusy_trigger) | |
| 905 $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); | |
| 906 | |
| 907 return $success ? $ret : false; | |
| 908 } | |
| 909 | |
| 910 /** | |
| 911 * Restore a single deleted event | |
| 912 * | |
| 913 * @param array Hash array with event properties: | |
| 914 * id: Event identifier | |
| 915 * @return boolean True on success, False on error | |
| 916 */ | |
| 917 public function restore_event($event) | |
| 918 { | |
| 919 if ($storage = $this->get_calendar($event['calendar'])) { | |
| 920 if (!empty($_SESSION['calendar_restore_event_data'])) | |
| 921 $success = $storage->update_event($_SESSION['calendar_restore_event_data']); | |
| 922 else | |
| 923 $success = $storage->restore_event($event); | |
| 924 | |
| 925 if ($success && $this->freebusy_trigger) | |
| 926 $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); | |
| 927 | |
| 928 return $success; | |
| 929 } | |
| 930 | |
| 931 return false; | |
| 932 } | |
| 933 | |
| 934 /** | |
| 935 * Wrapper to update an event object depending on the given savemode | |
| 936 */ | |
| 937 private function update_event($event) | |
| 938 { | |
| 939 if (!($storage = $this->get_calendar($event['calendar']))) | |
| 940 return false; | |
| 941 | |
| 942 // move event to another folder/calendar | |
| 943 if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) { | |
| 944 if (!($fromcalendar = $this->get_calendar($event['_fromcalendar']))) | |
| 945 return false; | |
| 946 | |
| 947 $old = $fromcalendar->get_event($event['id']); | |
| 948 | |
| 949 if ($event['_savemode'] != 'new') { | |
| 950 if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) { | |
| 951 return false; | |
| 952 } | |
| 953 | |
| 954 $fromcalendar = $storage; | |
| 955 } | |
| 956 } | |
| 957 else | |
| 958 $fromcalendar = $storage; | |
| 959 | |
| 960 $success = false; | |
| 961 $savemode = 'all'; | |
| 962 $attachments = array(); | |
| 963 $old = $master = $storage->get_event($event['id']); | |
| 964 | |
| 965 if (!$old || !$old['start']) { | |
| 966 rcube::raise_error(array( | |
| 967 'code' => 600, 'type' => 'php', | |
| 968 'file' => __FILE__, 'line' => __LINE__, | |
| 969 'message' => "Failed to load event object to update: id=" . $event['id']), | |
| 970 true, false); | |
| 971 return false; | |
| 972 } | |
| 973 | |
| 974 // modify a recurring event, check submitted savemode to do the right things | |
| 975 if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) { | |
| 976 $master = $storage->get_event($old['uid']); | |
| 977 $savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all'); | |
| 978 | |
| 979 // this-and-future on the first instance equals to 'all' | |
| 980 if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master)) | |
| 981 $savemode = 'all'; | |
| 982 // force 'current' mode for single occurrences stored as exception | |
| 983 else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception']) | |
| 984 $savemode = 'current'; | |
| 985 } | |
| 986 | |
| 987 // check if update affects scheduling and update attendee status accordingly | |
| 988 $reschedule = $this->check_scheduling($event, $old, true); | |
| 989 | |
| 990 // keep saved exceptions (not submitted by the client) | |
| 991 if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE'])) | |
| 992 $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; | |
| 993 if (isset($event['recurrence']['EXCEPTIONS'])) | |
| 994 $with_exceptions = true; // exceptions already provided (e.g. from iCal import) | |
| 995 else if ($old['recurrence']['EXCEPTIONS']) | |
| 996 $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS']; | |
| 997 else if ($old['exceptions']) | |
| 998 $event['exceptions'] = $old['exceptions']; | |
| 999 | |
| 1000 // remove some internal properties which should not be saved | |
| 1001 unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'], | |
| 1002 $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']); | |
| 1003 | |
| 1004 switch ($savemode) { | |
| 1005 case 'new': | |
| 1006 // save submitted data as new (non-recurring) event | |
| 1007 $event['recurrence'] = array(); | |
| 1008 $event['_copyfrom'] = $master['_msguid']; | |
| 1009 $event['_mailbox'] = $master['_mailbox']; | |
| 1010 $event['uid'] = $this->cal->generate_uid(); | |
| 1011 unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']); | |
| 1012 | |
| 1013 // copy attachment metadata to new event | |
| 1014 $event = self::from_rcube_event($event, $master); | |
| 1015 | |
| 1016 self::clear_attandee_noreply($event); | |
| 1017 if ($success = $storage->insert_event($event)) | |
| 1018 $success = $event['uid']; | |
| 1019 break; | |
| 1020 | |
| 1021 case 'future': | |
| 1022 // create a new recurring event | |
| 1023 $event['_copyfrom'] = $master['_msguid']; | |
| 1024 $event['_mailbox'] = $master['_mailbox']; | |
| 1025 $event['uid'] = $this->cal->generate_uid(); | |
| 1026 unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']); | |
| 1027 | |
| 1028 // copy attachment metadata to new event | |
| 1029 $event = self::from_rcube_event($event, $master); | |
| 1030 | |
| 1031 // remove recurrence exceptions on re-scheduling | |
| 1032 if ($reschedule) { | |
| 1033 unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']); | |
| 1034 } | |
| 1035 else if (is_array($event['recurrence']['EXCEPTIONS'])) { | |
| 1036 // only keep relevant exceptions | |
| 1037 $event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function($exception) use ($event) { | |
| 1038 return $exception['start'] > $event['start']; | |
| 1039 }); | |
| 1040 if (is_array($event['recurrence']['EXDATE'])) { | |
| 1041 $event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function($exdate) use ($event) { | |
| 1042 return $exdate > $event['start']; | |
| 1043 }); | |
| 1044 } | |
| 1045 // set link to top-level exceptions | |
| 1046 $event['exceptions'] = &$event['recurrence']['EXCEPTIONS']; | |
| 1047 } | |
| 1048 | |
| 1049 // compute remaining occurrences | |
| 1050 if ($event['recurrence']['COUNT']) { | |
| 1051 if (!$old['_count']) | |
| 1052 $old['_count'] = $this->get_recurrence_count($master, $old['start']); | |
| 1053 $event['recurrence']['COUNT'] -= intval($old['_count']); | |
| 1054 } | |
| 1055 | |
| 1056 // remove fixed weekday when date changed | |
| 1057 if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) { | |
| 1058 if (strlen($event['recurrence']['BYDAY']) == 2) | |
| 1059 unset($event['recurrence']['BYDAY']); | |
| 1060 if ($old['recurrence']['BYMONTH'] == $old['start']->format('n')) | |
| 1061 unset($event['recurrence']['BYMONTH']); | |
| 1062 } | |
| 1063 | |
| 1064 // set until-date on master event | |
| 1065 $master['recurrence']['UNTIL'] = clone $old['start']; | |
| 1066 $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); | |
| 1067 unset($master['recurrence']['COUNT']); | |
| 1068 | |
| 1069 // remove all exceptions after $event['start'] | |
| 1070 if (is_array($master['recurrence']['EXCEPTIONS'])) { | |
| 1071 $master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) { | |
| 1072 return $exception['start'] < $event['start']; | |
| 1073 }); | |
| 1074 // set link to top-level exceptions | |
| 1075 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS']; | |
| 1076 } | |
| 1077 if (is_array($master['recurrence']['EXDATE'])) { | |
| 1078 $master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) { | |
| 1079 return $exdate < $event['start']; | |
| 1080 }); | |
| 1081 } | |
| 1082 | |
| 1083 // save new event | |
| 1084 if ($success = $storage->insert_event($event)) { | |
| 1085 $success = $event['uid']; | |
| 1086 | |
| 1087 // update master event (no rescheduling!) | |
| 1088 self::clear_attandee_noreply($master); | |
| 1089 $storage->update_event($master); | |
| 1090 } | |
| 1091 break; | |
| 1092 | |
| 1093 case 'current': | |
| 1094 // recurring instances shall not store recurrence rules and attachments | |
| 1095 $event['recurrence'] = array(); | |
| 1096 $event['thisandfuture'] = $savemode == 'future'; | |
| 1097 unset($event['attachments'], $event['id']); | |
| 1098 | |
| 1099 // increment sequence of this instance if scheduling is affected | |
| 1100 if ($reschedule) { | |
| 1101 $event['sequence'] = max($old['sequence'], $master['sequence']) + 1; | |
| 1102 } | |
| 1103 else if (!isset($event['sequence'])) { | |
| 1104 $event['sequence'] = $old['sequence'] ?: $master['sequence']; | |
| 1105 } | |
| 1106 | |
| 1107 // save properties to a recurrence exception instance | |
| 1108 if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) { | |
| 1109 if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) { | |
| 1110 $success = $storage->update_event($master, $old['id']); | |
| 1111 break; | |
| 1112 } | |
| 1113 } | |
| 1114 | |
| 1115 $add_exception = true; | |
| 1116 | |
| 1117 // adjust matching RDATE entry if dates changed | |
| 1118 if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) { | |
| 1119 foreach ($master['recurrence']['RDATE'] as $j => $rdate) { | |
| 1120 if ($rdate->format('Ymd') == $old_date) { | |
| 1121 $master['recurrence']['RDATE'][$j] = $event['start']; | |
| 1122 sort($master['recurrence']['RDATE']); | |
| 1123 $add_exception = false; | |
| 1124 break; | |
| 1125 } | |
| 1126 } | |
| 1127 } | |
| 1128 | |
| 1129 // save as new exception to master event | |
| 1130 if ($add_exception) { | |
| 1131 self::add_exception($master, $event, $old); | |
| 1132 } | |
| 1133 | |
| 1134 $success = $storage->update_event($master); | |
| 1135 break; | |
| 1136 | |
| 1137 default: // 'all' is default | |
| 1138 $event['id'] = $master['uid']; | |
| 1139 $event['uid'] = $master['uid']; | |
| 1140 | |
| 1141 // use start date from master but try to be smart on time or duration changes | |
| 1142 $old_start_date = $old['start']->format('Y-m-d'); | |
| 1143 $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i'); | |
| 1144 $old_duration = self::event_duration($old['start'], $old['end'], $old['allday']); | |
| 1145 | |
| 1146 $new_start_date = $event['start']->format('Y-m-d'); | |
| 1147 $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i'); | |
| 1148 $new_duration = self::event_duration($event['start'], $event['end'], $event['allday']); | |
| 1149 | |
| 1150 $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; | |
| 1151 $date_shift = $old['start']->diff($event['start']); | |
| 1152 | |
| 1153 // shifted or resized | |
| 1154 if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { | |
| 1155 $event['start'] = $master['start']->add($date_shift); | |
| 1156 $event['end'] = clone $event['start']; | |
| 1157 $event['end']->add(new DateInterval($new_duration)); | |
| 1158 | |
| 1159 // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event() | |
| 1160 if ($old_start_date != $new_start_date) { | |
| 1161 if (strlen($event['recurrence']['BYDAY']) == 2) | |
| 1162 unset($event['recurrence']['BYDAY']); | |
| 1163 if ($old['recurrence']['BYMONTH'] == $old['start']->format('n')) | |
| 1164 unset($event['recurrence']['BYMONTH']); | |
| 1165 } | |
| 1166 } | |
| 1167 // dates did not change, use the ones from master | |
| 1168 else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) { | |
| 1169 $event['start'] = $master['start']; | |
| 1170 $event['end'] = $master['end']; | |
| 1171 } | |
| 1172 | |
| 1173 // when saving an instance in 'all' mode, copy recurrence exceptions over | |
| 1174 if ($old['recurrence_id']) { | |
| 1175 $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS']; | |
| 1176 $event['recurrence']['EXDATE'] = $master['recurrence']['EXDATE']; | |
| 1177 } | |
| 1178 else if ($master['_instance']) { | |
| 1179 $event['_instance'] = $master['_instance']; | |
| 1180 $event['recurrence_date'] = $master['recurrence_date']; | |
| 1181 } | |
| 1182 | |
| 1183 // TODO: forward changes to exceptions (which do not yet have differing values stored) | |
| 1184 if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) { | |
| 1185 // determine added and removed attendees | |
| 1186 $old_attendees = $current_attendees = $added_attendees = array(); | |
| 1187 foreach ((array)$old['attendees'] as $attendee) { | |
| 1188 $old_attendees[] = $attendee['email']; | |
| 1189 } | |
| 1190 foreach ((array)$event['attendees'] as $attendee) { | |
| 1191 $current_attendees[] = $attendee['email']; | |
| 1192 if (!in_array($attendee['email'], $old_attendees)) { | |
| 1193 $added_attendees[] = $attendee; | |
| 1194 } | |
| 1195 } | |
| 1196 $removed_attendees = array_diff($old_attendees, $current_attendees); | |
| 1197 | |
| 1198 foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { | |
| 1199 calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees); | |
| 1200 } | |
| 1201 | |
| 1202 // adjust recurrence-id when start changed and therefore the entire recurrence chain changes | |
| 1203 if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) { | |
| 1204 $recurrence_id_format = libcalendaring::recurrence_id_format($event); | |
| 1205 foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { | |
| 1206 $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] : | |
| 1207 rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone()); | |
| 1208 if (is_a($recurrence_id, 'DateTime')) { | |
| 1209 $recurrence_id->add($date_shift); | |
| 1210 $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id; | |
| 1211 $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format); | |
| 1212 } | |
| 1213 } | |
| 1214 } | |
| 1215 | |
| 1216 // set link to top-level exceptions | |
| 1217 $event['exceptions'] = &$event['recurrence']['EXCEPTIONS']; | |
| 1218 } | |
| 1219 | |
| 1220 // unset _dateonly flags in (cached) date objects | |
| 1221 unset($event['start']->_dateonly, $event['end']->_dateonly); | |
| 1222 | |
| 1223 $success = $storage->update_event($event) ? $event['id'] : false; // return master UID | |
| 1224 break; | |
| 1225 } | |
| 1226 | |
| 1227 if ($success && $this->freebusy_trigger) | |
| 1228 $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); | |
| 1229 | |
| 1230 return $success; | |
| 1231 } | |
| 1232 | |
| 1233 /** | |
| 1234 * Calculate event duration, returns string in DateInterval format | |
| 1235 */ | |
| 1236 protected static function event_duration($start, $end, $allday = false) | |
| 1237 { | |
| 1238 if ($allday) { | |
| 1239 $diff = $start->diff($end); | |
| 1240 return 'P' . $diff->days . 'D'; | |
| 1241 } | |
| 1242 | |
| 1243 return 'PT' . ($end->format('U') - $start->format('U')) . 'S'; | |
| 1244 } | |
| 1245 | |
| 1246 /** | |
| 1247 * Determine whether the current change affects scheduling and reset attendee status accordingly | |
| 1248 */ | |
| 1249 public function check_scheduling(&$event, $old, $update = true) | |
| 1250 { | |
| 1251 // skip this check when importing iCal/iTip events | |
| 1252 if (isset($event['sequence']) || !empty($event['_method'])) { | |
| 1253 return false; | |
| 1254 } | |
| 1255 | |
| 1256 // iterate through the list of properties considered 'significant' for scheduling | |
| 1257 $kolab_event = $old['_formatobj'] ?: new kolab_format_event(); | |
| 1258 $reschedule = $kolab_event->check_rescheduling($event, $old); | |
| 1259 | |
| 1260 // reset all attendee status to needs-action (#4360) | |
| 1261 if ($update && $reschedule && is_array($event['attendees'])) { | |
| 1262 $is_organizer = false; | |
| 1263 $emails = $this->cal->get_user_emails(); | |
| 1264 $attendees = $event['attendees']; | |
| 1265 foreach ($attendees as $i => $attendee) { | |
| 1266 if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { | |
| 1267 $is_organizer = true; | |
| 1268 } | |
| 1269 else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') { | |
| 1270 $attendees[$i]['status'] = 'NEEDS-ACTION'; | |
| 1271 $attendees[$i]['rsvp'] = true; | |
| 1272 } | |
| 1273 } | |
| 1274 | |
| 1275 // update attendees only if I'm the organizer | |
| 1276 if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) { | |
| 1277 $event['attendees'] = $attendees; | |
| 1278 } | |
| 1279 } | |
| 1280 | |
| 1281 return $reschedule; | |
| 1282 } | |
| 1283 | |
| 1284 /** | |
| 1285 * Apply the given changes to already existing exceptions | |
| 1286 */ | |
| 1287 protected function update_recurrence_exceptions(&$master, $event, $old, $savemode) | |
| 1288 { | |
| 1289 $saved = false; | |
| 1290 $existing = null; | |
| 1291 | |
| 1292 // determine added and removed attendees | |
| 1293 $added_attendees = $removed_attendees = array(); | |
| 1294 if ($savemode == 'future') { | |
| 1295 $old_attendees = $current_attendees = array(); | |
| 1296 foreach ((array)$old['attendees'] as $attendee) { | |
| 1297 $old_attendees[] = $attendee['email']; | |
| 1298 } | |
| 1299 foreach ((array)$event['attendees'] as $attendee) { | |
| 1300 $current_attendees[] = $attendee['email']; | |
| 1301 if (!in_array($attendee['email'], $old_attendees)) { | |
| 1302 $added_attendees[] = $attendee; | |
| 1303 } | |
| 1304 } | |
| 1305 $removed_attendees = array_diff($old_attendees, $current_attendees); | |
| 1306 } | |
| 1307 | |
| 1308 foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) { | |
| 1309 // update a specific instance | |
| 1310 if ($exception['_instance'] == $old['_instance']) { | |
| 1311 $existing = $i; | |
| 1312 | |
| 1313 // check savemode against existing exception mode. | |
| 1314 // if matches, we can update this existing exception | |
| 1315 if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) { | |
| 1316 $event['_instance'] = $old['_instance']; | |
| 1317 $event['thisandfuture'] = $old['thisandfuture']; | |
| 1318 $event['recurrence_date'] = $old['recurrence_date']; | |
| 1319 $master['recurrence']['EXCEPTIONS'][$i] = $event; | |
| 1320 $saved = true; | |
| 1321 } | |
| 1322 } | |
| 1323 // merge the new event properties onto future exceptions | |
| 1324 if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) { | |
| 1325 unset($event['thisandfuture']); | |
| 1326 self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees')); | |
| 1327 | |
| 1328 if (!empty($added_attendees) || !empty($removed_attendees)) { | |
| 1329 calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees); | |
| 1330 } | |
| 1331 } | |
| 1332 } | |
| 1333 /* | |
| 1334 // we could not update the existing exception due to savemode mismatch... | |
| 1335 if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) { | |
| 1336 // ... try to move the existing this-and-future exception to the next occurrence | |
| 1337 foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) { | |
| 1338 // our old this-and-future exception is obsolete | |
| 1339 if ($candidate['thisandfuture']) { | |
| 1340 unset($master['recurrence']['EXCEPTIONS'][$existing]); | |
| 1341 $saved = true; | |
| 1342 break; | |
| 1343 } | |
| 1344 // this occurrence doesn't yet have an exception | |
| 1345 else if (!$candidate['isexception']) { | |
| 1346 $event['_instance'] = $candidate['_instance']; | |
| 1347 $event['recurrence_date'] = $candidate['recurrence_date']; | |
| 1348 $master['recurrence']['EXCEPTIONS'][$i] = $event; | |
| 1349 $saved = true; | |
| 1350 break; | |
| 1351 } | |
| 1352 } | |
| 1353 } | |
| 1354 */ | |
| 1355 | |
| 1356 // set link to top-level exceptions | |
| 1357 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS']; | |
| 1358 | |
| 1359 // returning false here will add a new exception | |
| 1360 return $saved; | |
| 1361 } | |
| 1362 | |
| 1363 /** | |
| 1364 * Add or update the given event as an exception to $master | |
| 1365 */ | |
| 1366 public static function add_exception(&$master, $event, $old = null) | |
| 1367 { | |
| 1368 if ($old) { | |
| 1369 $event['_instance'] = $old['_instance']; | |
| 1370 if (!$event['recurrence_date']) | |
| 1371 $event['recurrence_date'] = $old['recurrence_date'] ?: $old['start']; | |
| 1372 } | |
| 1373 else if (!$event['recurrence_date']) { | |
| 1374 $event['recurrence_date'] = $event['start']; | |
| 1375 } | |
| 1376 | |
| 1377 if (!$event['_instance'] && is_a($event['recurrence_date'], 'DateTime')) { | |
| 1378 $event['_instance'] = libcalendaring::recurrence_instance_identifier($event, $master['allday']); | |
| 1379 } | |
| 1380 | |
| 1381 if (!is_array($master['exceptions']) && is_array($master['recurrence']['EXCEPTIONS'])) { | |
| 1382 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS']; | |
| 1383 } | |
| 1384 | |
| 1385 $existing = false; | |
| 1386 foreach ((array)$master['exceptions'] as $i => $exception) { | |
| 1387 if ($exception['_instance'] == $event['_instance']) { | |
| 1388 $master['exceptions'][$i] = $event; | |
| 1389 $existing = true; | |
| 1390 } | |
| 1391 } | |
| 1392 | |
| 1393 if (!$existing) { | |
| 1394 $master['exceptions'][] = $event; | |
| 1395 } | |
| 1396 | |
| 1397 return true; | |
| 1398 } | |
| 1399 | |
| 1400 /** | |
| 1401 * Remove the noreply flags from attendees | |
| 1402 */ | |
| 1403 public static function clear_attandee_noreply(&$event) | |
| 1404 { | |
| 1405 foreach ((array)$event['attendees'] as $i => $attendee) { | |
| 1406 unset($event['attendees'][$i]['noreply']); | |
| 1407 } | |
| 1408 } | |
| 1409 | |
| 1410 /** | |
| 1411 * Merge certain properties from the overlay event to the base event object | |
| 1412 * | |
| 1413 * @param array The event object to be altered | |
| 1414 * @param array The overlay event object to be merged over $event | |
| 1415 * @param array List of properties not allowed to be overwritten | |
| 1416 */ | |
| 1417 public static function merge_exception_data(&$event, $overlay, $blacklist = null) | |
| 1418 { | |
| 1419 $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments'); | |
| 1420 | |
| 1421 if (is_array($blacklist)) | |
| 1422 $forbidden = array_merge($forbidden, $blacklist); | |
| 1423 | |
| 1424 foreach ($overlay as $prop => $value) { | |
| 1425 if ($prop == 'start' || $prop == 'end') { | |
| 1426 // handled by merge_exception_dates() below | |
| 1427 } | |
| 1428 else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) { | |
| 1429 $event[$prop] = $value; | |
| 1430 } | |
| 1431 else if ($prop[0] != '_' && !in_array($prop, $forbidden)) | |
| 1432 $event[$prop] = $value; | |
| 1433 } | |
| 1434 | |
| 1435 self::merge_exception_dates($event, $overlay); | |
| 1436 } | |
| 1437 | |
| 1438 /** | |
| 1439 * Merge start/end date from the overlay event to the base event object | |
| 1440 * | |
| 1441 * @param array The event object to be altered | |
| 1442 * @param array The overlay event object to be merged over $event | |
| 1443 */ | |
| 1444 public static function merge_exception_dates(&$event, $overlay) | |
| 1445 { | |
| 1446 // compute date offset from the exception | |
| 1447 if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) { | |
| 1448 $date_offset = $overlay['recurrence_date']->diff($overlay['start']); | |
| 1449 } | |
| 1450 | |
| 1451 foreach (array('start', 'end') as $prop) { | |
| 1452 $value = $overlay[$prop]; | |
| 1453 if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) { | |
| 1454 // set date value if overlay is an exception of the current instance | |
| 1455 if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) { | |
| 1456 $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j'))); | |
| 1457 } | |
| 1458 // apply date offset | |
| 1459 else if ($date_offset) { | |
| 1460 $event[$prop]->add($date_offset); | |
| 1461 } | |
| 1462 // adjust time of the recurring event instance | |
| 1463 $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s'))); | |
| 1464 } | |
| 1465 } | |
| 1466 } | |
| 1467 | |
| 1468 /** | |
| 1469 * Get events from source. | |
| 1470 * | |
| 1471 * @param integer Event's new start (unix timestamp) | |
| 1472 * @param integer Event's new end (unix timestamp) | |
| 1473 * @param string Search query (optional) | |
| 1474 * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) | |
| 1475 * @param boolean Include virtual events (optional) | |
| 1476 * @param integer Only list events modified since this time (unix timestamp) | |
| 1477 * @return array A list of event records | |
| 1478 */ | |
| 1479 public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null) | |
| 1480 { | |
| 1481 if ($calendars && is_string($calendars)) | |
| 1482 $calendars = explode(',', $calendars); | |
| 1483 else if (!$calendars) { | |
| 1484 $this->_read_calendars(); | |
| 1485 $calendars = array_keys($this->calendars); | |
| 1486 } | |
| 1487 | |
| 1488 $query = array(); | |
| 1489 if ($modifiedsince) | |
| 1490 $query[] = array('changed', '>=', $modifiedsince); | |
| 1491 | |
| 1492 $events = $categories = array(); | |
| 1493 foreach ($calendars as $cid) { | |
| 1494 if ($storage = $this->get_calendar($cid)) { | |
| 1495 $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query)); | |
| 1496 $categories += $storage->categories; | |
| 1497 } | |
| 1498 } | |
| 1499 | |
| 1500 // add events from the address books birthday calendar | |
| 1501 if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) { | |
| 1502 $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince)); | |
| 1503 } | |
| 1504 | |
| 1505 // add new categories to user prefs | |
| 1506 $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories); | |
| 1507 if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) { | |
| 1508 foreach ($newcats as $category) | |
| 1509 $old_categories[$category] = ''; // no color set yet | |
| 1510 $this->rc->user->save_prefs(array('calendar_categories' => $old_categories)); | |
| 1511 } | |
| 1512 | |
| 1513 array_walk($events, 'kolab_driver::to_rcube_event'); | |
| 1514 return $events; | |
| 1515 } | |
| 1516 | |
| 1517 /** | |
| 1518 * Get number of events in the given calendar | |
| 1519 * | |
| 1520 * @param mixed List of calendar IDs to count events (either as array or comma-separated string) | |
| 1521 * @param integer Date range start (unix timestamp) | |
| 1522 * @param integer Date range end (unix timestamp) | |
| 1523 * @return array Hash array with counts grouped by calendar ID | |
| 1524 */ | |
| 1525 public function count_events($calendars, $start, $end = null) | |
| 1526 { | |
| 1527 $counts = array(); | |
| 1528 | |
| 1529 if ($calendars && is_string($calendars)) | |
| 1530 $calendars = explode(',', $calendars); | |
| 1531 else if (!$calendars) { | |
| 1532 $this->_read_calendars(); | |
| 1533 $calendars = array_keys($this->calendars); | |
| 1534 } | |
| 1535 | |
| 1536 foreach ($calendars as $cid) { | |
| 1537 if ($storage = $this->get_calendar($cid)) { | |
| 1538 $counts[$cid] = $storage->count_events($start, $end); | |
| 1539 } | |
| 1540 } | |
| 1541 | |
| 1542 return $counts; | |
| 1543 } | |
| 1544 | |
| 1545 /** | |
| 1546 * Get a list of pending alarms to be displayed to the user | |
| 1547 * | |
| 1548 * @see calendar_driver::pending_alarms() | |
| 1549 */ | |
| 1550 public function pending_alarms($time, $calendars = null) | |
| 1551 { | |
| 1552 $interval = 300; | |
| 1553 $time -= $time % 60; | |
| 1554 | |
| 1555 $slot = $time; | |
| 1556 $slot -= $slot % $interval; | |
| 1557 | |
| 1558 $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); | |
| 1559 $last -= $last % $interval; | |
| 1560 | |
| 1561 // only check for alerts once in 5 minutes | |
| 1562 if ($last == $slot) | |
| 1563 return array(); | |
| 1564 | |
| 1565 if ($calendars && is_string($calendars)) | |
| 1566 $calendars = explode(',', $calendars); | |
| 1567 | |
| 1568 $time = $slot + $interval; | |
| 1569 | |
| 1570 $candidates = array(); | |
| 1571 $query = array(array('tags', '=', 'x-has-alarms')); | |
| 1572 | |
| 1573 $this->_read_calendars(); | |
| 1574 | |
| 1575 foreach ($this->calendars as $cid => $calendar) { | |
| 1576 // skip calendars with alarms disabled | |
| 1577 if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars))) | |
| 1578 continue; | |
| 1579 | |
| 1580 foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) { | |
| 1581 // add to list if alarm is set | |
| 1582 $alarm = libcalendaring::get_next_alarm($e); | |
| 1583 if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) { | |
| 1584 $id = $alarm['id']; // use alarm-id as primary identifier | |
| 1585 $candidates[$id] = array( | |
| 1586 'id' => $id, | |
| 1587 'title' => $e['title'], | |
| 1588 'location' => $e['location'], | |
| 1589 'start' => $e['start'], | |
| 1590 'end' => $e['end'], | |
| 1591 'notifyat' => $alarm['time'], | |
| 1592 'action' => $alarm['action'], | |
| 1593 ); | |
| 1594 } | |
| 1595 } | |
| 1596 } | |
| 1597 | |
| 1598 // get alarm information stored in local database | |
| 1599 if (!empty($candidates)) { | |
| 1600 $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); | |
| 1601 $result = $this->rc->db->query("SELECT *" | |
| 1602 . " FROM " . $this->rc->db->table_name('kolab_alarms', true) | |
| 1603 . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")" | |
| 1604 . " AND `user_id` = ?", | |
| 1605 $this->rc->user->ID | |
| 1606 ); | |
| 1607 | |
| 1608 while ($result && ($e = $this->rc->db->fetch_assoc($result))) { | |
| 1609 $dbdata[$e['alarm_id']] = $e; | |
| 1610 } | |
| 1611 } | |
| 1612 | |
| 1613 $alarms = array(); | |
| 1614 foreach ($candidates as $id => $alarm) { | |
| 1615 // skip dismissed alarms | |
| 1616 if ($dbdata[$id]['dismissed']) | |
| 1617 continue; | |
| 1618 | |
| 1619 // snooze function may have shifted alarm time | |
| 1620 $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat']; | |
| 1621 if ($notifyat <= $time) | |
| 1622 $alarms[] = $alarm; | |
| 1623 } | |
| 1624 | |
| 1625 return $alarms; | |
| 1626 } | |
| 1627 | |
| 1628 /** | |
| 1629 * Feedback after showing/sending an alarm notification | |
| 1630 * | |
| 1631 * @see calendar_driver::dismiss_alarm() | |
| 1632 */ | |
| 1633 public function dismiss_alarm($alarm_id, $snooze = 0) | |
| 1634 { | |
| 1635 $alarms_table = $this->rc->db->table_name('kolab_alarms', true); | |
| 1636 // delete old alarm entry | |
| 1637 $this->rc->db->query("DELETE FROM $alarms_table" | |
| 1638 . " WHERE `alarm_id` = ? AND `user_id` = ?", | |
| 1639 $alarm_id, | |
| 1640 $this->rc->user->ID | |
| 1641 ); | |
| 1642 | |
| 1643 // set new notifyat time or unset if not snoozed | |
| 1644 $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; | |
| 1645 | |
| 1646 $query = $this->rc->db->query("INSERT INTO $alarms_table" | |
| 1647 . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)" | |
| 1648 . " VALUES (?, ?, ?, ?)", | |
| 1649 $alarm_id, | |
| 1650 $this->rc->user->ID, | |
| 1651 $snooze > 0 ? 0 : 1, | |
| 1652 $notifyat | |
| 1653 ); | |
| 1654 | |
| 1655 return $this->rc->db->affected_rows($query); | |
| 1656 } | |
| 1657 | |
| 1658 /** | |
| 1659 * List attachments from the given event | |
| 1660 */ | |
| 1661 public function list_attachments($event) | |
| 1662 { | |
| 1663 if (!($storage = $this->get_calendar($event['calendar']))) | |
| 1664 return false; | |
| 1665 | |
| 1666 $event = $storage->get_event($event['id']); | |
| 1667 | |
| 1668 return $event['attachments']; | |
| 1669 } | |
| 1670 | |
| 1671 /** | |
| 1672 * Get attachment properties | |
| 1673 */ | |
| 1674 public function get_attachment($id, $event) | |
| 1675 { | |
| 1676 if (!($storage = $this->get_calendar($event['calendar']))) | |
| 1677 return false; | |
| 1678 | |
| 1679 // get old revision of event | |
| 1680 if ($event['rev']) { | |
| 1681 $event = $this->get_event_revison($event, $event['rev'], true); | |
| 1682 } | |
| 1683 else { | |
| 1684 $event = $storage->get_event($event['id']); | |
| 1685 } | |
| 1686 | |
| 1687 if ($event) { | |
| 1688 $attachments = isset($event['_attachments']) ? $event['_attachments'] : $event['attachments']; | |
| 1689 foreach ((array) $attachments as $att) { | |
| 1690 if ($att['id'] == $id) { | |
| 1691 return $att; | |
| 1692 } | |
| 1693 } | |
| 1694 } | |
| 1695 } | |
| 1696 | |
| 1697 /** | |
| 1698 * Get attachment body | |
| 1699 * @see calendar_driver::get_attachment_body() | |
| 1700 */ | |
| 1701 public function get_attachment_body($id, $event) | |
| 1702 { | |
| 1703 if (!($cal = $this->get_calendar($event['calendar']))) | |
| 1704 return false; | |
| 1705 | |
| 1706 // get old revision of event | |
| 1707 if ($event['rev']) { | |
| 1708 if (empty($this->bonnie_api)) { | |
| 1709 return false; | |
| 1710 } | |
| 1711 | |
| 1712 $cid = substr($id, 4); | |
| 1713 | |
| 1714 // call Bonnie API and get the raw mime message | |
| 1715 list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event); | |
| 1716 if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['rev'], $mailbox, $msguid)) { | |
| 1717 // parse the message and find the part with the matching content-id | |
| 1718 $message = rcube_mime::parse_message($msg_raw); | |
| 1719 foreach ((array)$message->parts as $part) { | |
| 1720 if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) { | |
| 1721 return $part->body; | |
| 1722 } | |
| 1723 } | |
| 1724 } | |
| 1725 | |
| 1726 return false; | |
| 1727 } | |
| 1728 | |
| 1729 return $cal->get_attachment_body($id, $event); | |
| 1730 } | |
| 1731 | |
| 1732 /** | |
| 1733 * Build a struct representing the given message reference | |
| 1734 * | |
| 1735 * @see calendar_driver::get_message_reference() | |
| 1736 */ | |
| 1737 public function get_message_reference($uri_or_headers, $folder = null) | |
| 1738 { | |
| 1739 if (is_object($uri_or_headers)) { | |
| 1740 $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder); | |
| 1741 } | |
| 1742 | |
| 1743 if (is_string($uri_or_headers)) { | |
| 1744 return kolab_storage_config::get_message_reference($uri_or_headers, 'event'); | |
| 1745 } | |
| 1746 | |
| 1747 return false; | |
| 1748 } | |
| 1749 | |
| 1750 /** | |
| 1751 * List availabale categories | |
| 1752 * The default implementation reads them from config/user prefs | |
| 1753 */ | |
| 1754 public function list_categories() | |
| 1755 { | |
| 1756 // FIXME: complete list with categories saved in config objects (KEP:12) | |
| 1757 return $this->rc->config->get('calendar_categories', $this->default_categories); | |
| 1758 } | |
| 1759 | |
| 1760 /** | |
| 1761 * Create instances of a recurring event | |
| 1762 * | |
| 1763 * @param array Hash array with event properties | |
| 1764 * @param object DateTime Start date of the recurrence window | |
| 1765 * @param object DateTime End date of the recurrence window | |
| 1766 * @return array List of recurring event instances | |
| 1767 */ | |
| 1768 public function get_recurring_events($event, $start, $end = null) | |
| 1769 { | |
| 1770 // load the given event data into a libkolabxml container | |
| 1771 if (!$event['_formatobj']) { | |
| 1772 $event_xml = new kolab_format_event(); | |
| 1773 $event_xml->set($event); | |
| 1774 $event['_formatobj'] = $event_xml; | |
| 1775 } | |
| 1776 | |
| 1777 $this->_read_calendars(); | |
| 1778 $storage = reset($this->calendars); | |
| 1779 return $storage->get_recurring_events($event, $start, $end); | |
| 1780 } | |
| 1781 | |
| 1782 /** | |
| 1783 * | |
| 1784 */ | |
| 1785 private function get_recurrence_count($event, $dtstart) | |
| 1786 { | |
| 1787 // use libkolab to compute recurring events | |
| 1788 if (class_exists('kolabcalendaring') && $event['_formatobj']) { | |
| 1789 $recurrence = new kolab_date_recurrence($event['_formatobj']); | |
| 1790 } | |
| 1791 else { | |
| 1792 // fallback to local recurrence implementation | |
| 1793 require_once($this->cal->home . '/lib/calendar_recurrence.php'); | |
| 1794 $recurrence = new calendar_recurrence($this->cal, $event); | |
| 1795 } | |
| 1796 | |
| 1797 $count = 0; | |
| 1798 while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) { | |
| 1799 $count++; | |
| 1800 } | |
| 1801 | |
| 1802 return $count; | |
| 1803 } | |
| 1804 | |
| 1805 /** | |
| 1806 * Fetch free/busy information from a person within the given range | |
| 1807 */ | |
| 1808 public function get_freebusy_list($email, $start, $end) | |
| 1809 { | |
| 1810 if (empty($email)/* || $end < time()*/) | |
| 1811 return false; | |
| 1812 | |
| 1813 // map vcalendar fbtypes to internal values | |
| 1814 $fbtypemap = array( | |
| 1815 'FREE' => calendar::FREEBUSY_FREE, | |
| 1816 'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE, | |
| 1817 'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF, | |
| 1818 'OOF' => calendar::FREEBUSY_OOF); | |
| 1819 | |
| 1820 // ask kolab server first | |
| 1821 try { | |
| 1822 $request_config = array( | |
| 1823 'store_body' => true, | |
| 1824 'follow_redirects' => true, | |
| 1825 ); | |
| 1826 $request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config); | |
| 1827 $response = $request->send(); | |
| 1828 | |
| 1829 // authentication required | |
| 1830 if ($response->getStatus() == 401) { | |
| 1831 $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password'])); | |
| 1832 $response = $request->send(); | |
| 1833 } | |
| 1834 | |
| 1835 if ($response->getStatus() == 200) | |
| 1836 $fbdata = $response->getBody(); | |
| 1837 | |
| 1838 unset($request, $response); | |
| 1839 } | |
| 1840 catch (Exception $e) { | |
| 1841 PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage()); | |
| 1842 } | |
| 1843 | |
| 1844 // get free-busy url from contacts | |
| 1845 if (!$fbdata) { | |
| 1846 $fburl = null; | |
| 1847 foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) { | |
| 1848 $abook = $this->rc->get_address_book($book); | |
| 1849 | |
| 1850 if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) { | |
| 1851 while ($contact = $result->iterate()) { | |
| 1852 if ($fburl = $contact['freebusyurl']) { | |
| 1853 $fbdata = @file_get_contents($fburl); | |
| 1854 break; | |
| 1855 } | |
| 1856 } | |
| 1857 } | |
| 1858 | |
| 1859 if ($fbdata) | |
| 1860 break; | |
| 1861 } | |
| 1862 } | |
| 1863 | |
| 1864 // parse free-busy information using Horde classes | |
| 1865 if ($fbdata) { | |
| 1866 $ical = $this->cal->get_ical(); | |
| 1867 $ical->import($fbdata); | |
| 1868 if ($fb = $ical->freebusy) { | |
| 1869 $result = array(); | |
| 1870 foreach ($fb['periods'] as $tuple) { | |
| 1871 list($from, $to, $type) = $tuple; | |
| 1872 $result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY); | |
| 1873 } | |
| 1874 | |
| 1875 // we take 'dummy' free-busy lists as "unknown" | |
| 1876 if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy')) | |
| 1877 return false; | |
| 1878 | |
| 1879 // set period from $start till the begin of the free-busy information as 'unknown' | |
| 1880 if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) { | |
| 1881 array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN)); | |
| 1882 } | |
| 1883 // pad period till $end with status 'unknown' | |
| 1884 if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) { | |
| 1885 $result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN); | |
| 1886 } | |
| 1887 | |
| 1888 return $result; | |
| 1889 } | |
| 1890 } | |
| 1891 | |
| 1892 return false; | |
| 1893 } | |
| 1894 | |
| 1895 /** | |
| 1896 * Handler to push folder triggers when sent from client. | |
| 1897 * Used to push free-busy changes asynchronously after updating an event | |
| 1898 */ | |
| 1899 public function push_freebusy() | |
| 1900 { | |
| 1901 // make shure triggering completes | |
| 1902 set_time_limit(0); | |
| 1903 ignore_user_abort(true); | |
| 1904 | |
| 1905 $cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); | |
| 1906 if (!($cal = $this->get_calendar($cal))) | |
| 1907 return false; | |
| 1908 | |
| 1909 // trigger updates on folder | |
| 1910 $trigger = $cal->storage->trigger(); | |
| 1911 if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) { | |
| 1912 rcube::raise_error(array( | |
| 1913 'code' => 900, 'type' => 'php', | |
| 1914 'file' => __FILE__, 'line' => __LINE__, | |
| 1915 'message' => "Failed triggering folder. Error was " . $trigger->getMessage()), | |
| 1916 true, false); | |
| 1917 } | |
| 1918 | |
| 1919 exit; | |
| 1920 } | |
| 1921 | |
| 1922 | |
| 1923 /** | |
| 1924 * Convert from driver format to external caledar app data | |
| 1925 */ | |
| 1926 public static function to_rcube_event(&$record) | |
| 1927 { | |
| 1928 if (!is_array($record)) | |
| 1929 return $record; | |
| 1930 | |
| 1931 $record['id'] = $record['uid']; | |
| 1932 | |
| 1933 if ($record['_instance']) { | |
| 1934 $record['id'] .= '-' . $record['_instance']; | |
| 1935 | |
| 1936 if (!$record['recurrence_id'] && !empty($record['recurrence'])) | |
| 1937 $record['recurrence_id'] = $record['uid']; | |
| 1938 } | |
| 1939 | |
| 1940 // all-day events go from 12:00 - 13:00 | |
| 1941 if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) { | |
| 1942 $record['end'] = clone $record['start']; | |
| 1943 $record['end']->add(new DateInterval('PT1H')); | |
| 1944 } | |
| 1945 | |
| 1946 // translate internal '_attachments' to external 'attachments' list | |
| 1947 if (!empty($record['_attachments'])) { | |
| 1948 foreach ($record['_attachments'] as $key => $attachment) { | |
| 1949 if ($attachment !== false) { | |
| 1950 if (!$attachment['name']) | |
| 1951 $attachment['name'] = $key; | |
| 1952 | |
| 1953 unset($attachment['path'], $attachment['content']); | |
| 1954 $attachments[] = $attachment; | |
| 1955 } | |
| 1956 } | |
| 1957 | |
| 1958 $record['attachments'] = $attachments; | |
| 1959 } | |
| 1960 | |
| 1961 if (!empty($record['attendees'])) { | |
| 1962 foreach ((array)$record['attendees'] as $i => $attendee) { | |
| 1963 if (is_array($attendee['delegated-from'])) { | |
| 1964 $record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']); | |
| 1965 } | |
| 1966 if (is_array($attendee['delegated-to'])) { | |
| 1967 $record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']); | |
| 1968 } | |
| 1969 } | |
| 1970 } | |
| 1971 | |
| 1972 // Roundcube only supports one category assignment | |
| 1973 if (is_array($record['categories'])) | |
| 1974 $record['categories'] = $record['categories'][0]; | |
| 1975 | |
| 1976 // the cancelled flag transltes into status=CANCELLED | |
| 1977 if ($record['cancelled']) | |
| 1978 $record['status'] = 'CANCELLED'; | |
| 1979 | |
| 1980 // The web client only supports DISPLAY type of alarms | |
| 1981 if (!empty($record['alarms'])) | |
| 1982 $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']); | |
| 1983 | |
| 1984 // remove empty recurrence array | |
| 1985 if (empty($record['recurrence'])) | |
| 1986 unset($record['recurrence']); | |
| 1987 | |
| 1988 // clean up exception data | |
| 1989 if (is_array($record['recurrence']['EXCEPTIONS'])) { | |
| 1990 array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) { | |
| 1991 unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']); | |
| 1992 }); | |
| 1993 } | |
| 1994 | |
| 1995 unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'], | |
| 1996 $record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']); | |
| 1997 | |
| 1998 return $record; | |
| 1999 } | |
| 2000 | |
| 2001 /** | |
| 2002 * | |
| 2003 */ | |
| 2004 public static function from_rcube_event($event, $old = array()) | |
| 2005 { | |
| 2006 kolab_format::merge_attachments($event, $old); | |
| 2007 | |
| 2008 return $event; | |
| 2009 } | |
| 2010 | |
| 2011 | |
| 2012 /** | |
| 2013 * Set CSS class according to the event's attendde partstat | |
| 2014 */ | |
| 2015 public static function add_partstat_class($event, $partstats, $user = null) | |
| 2016 { | |
| 2017 // set classes according to PARTSTAT | |
| 2018 if (is_array($event['attendees'])) { | |
| 2019 $user_emails = libcalendaring::get_instance()->get_user_emails($user); | |
| 2020 $partstat = 'UNKNOWN'; | |
| 2021 foreach ($event['attendees'] as $attendee) { | |
| 2022 if (in_array($attendee['email'], $user_emails)) { | |
| 2023 $partstat = $attendee['status']; | |
| 2024 break; | |
| 2025 } | |
| 2026 } | |
| 2027 | |
| 2028 if (in_array($partstat, $partstats)) { | |
| 2029 $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat)); | |
| 2030 } | |
| 2031 } | |
| 2032 | |
| 2033 return $event; | |
| 2034 } | |
| 2035 | |
| 2036 /** | |
| 2037 * Provide a list of revisions for the given event | |
| 2038 * | |
| 2039 * @param array $event Hash array with event properties | |
| 2040 * | |
| 2041 * @return array List of changes, each as a hash array | |
| 2042 * @see calendar_driver::get_event_changelog() | |
| 2043 */ | |
| 2044 public function get_event_changelog($event) | |
| 2045 { | |
| 2046 if (empty($this->bonnie_api)) { | |
| 2047 return false; | |
| 2048 } | |
| 2049 | |
| 2050 list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event); | |
| 2051 | |
| 2052 $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid); | |
| 2053 if (is_array($result) && $result['uid'] == $uid) { | |
| 2054 return $result['changes']; | |
| 2055 } | |
| 2056 | |
| 2057 return false; | |
| 2058 } | |
| 2059 | |
| 2060 /** | |
| 2061 * Get a list of property changes beteen two revisions of an event | |
| 2062 * | |
| 2063 * @param array $event Hash array with event properties | |
| 2064 * @param mixed $rev1 Old Revision | |
| 2065 * @param mixed $rev2 New Revision | |
| 2066 * | |
| 2067 * @return array List of property changes, each as a hash array | |
| 2068 * @see calendar_driver::get_event_diff() | |
| 2069 */ | |
| 2070 public function get_event_diff($event, $rev1, $rev2) | |
| 2071 { | |
| 2072 if (empty($this->bonnie_api)) { | |
| 2073 return false; | |
| 2074 } | |
| 2075 | |
| 2076 list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event); | |
| 2077 | |
| 2078 // get diff for the requested recurrence instance | |
| 2079 $instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null; | |
| 2080 | |
| 2081 // call Bonnie API | |
| 2082 $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id); | |
| 2083 if (is_array($result) && $result['uid'] == $uid) { | |
| 2084 $result['rev1'] = $rev1; | |
| 2085 $result['rev2'] = $rev2; | |
| 2086 | |
| 2087 $keymap = array( | |
| 2088 'dtstart' => 'start', | |
| 2089 'dtend' => 'end', | |
| 2090 'dstamp' => 'changed', | |
| 2091 'summary' => 'title', | |
| 2092 'alarm' => 'alarms', | |
| 2093 'attendee' => 'attendees', | |
| 2094 'attach' => 'attachments', | |
| 2095 'rrule' => 'recurrence', | |
| 2096 'transparency' => 'free_busy', | |
| 2097 'classification' => 'sensitivity', | |
| 2098 'lastmodified-date' => 'changed', | |
| 2099 ); | |
| 2100 $prop_keymaps = array( | |
| 2101 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'), | |
| 2102 'attendees' => array('partstat' => 'status'), | |
| 2103 ); | |
| 2104 $special_changes = array(); | |
| 2105 | |
| 2106 // map kolab event properties to keys the client expects | |
| 2107 array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) { | |
| 2108 if (array_key_exists($change['property'], $keymap)) { | |
| 2109 $change['property'] = $keymap[$change['property']]; | |
| 2110 } | |
| 2111 // translate free_busy values | |
| 2112 if ($change['property'] == 'free_busy') { | |
| 2113 $change['old'] = $old['old'] ? 'free' : 'busy'; | |
| 2114 $change['new'] = $old['new'] ? 'free' : 'busy'; | |
| 2115 } | |
| 2116 // map alarms trigger value | |
| 2117 if ($change['property'] == 'alarms') { | |
| 2118 if (is_array($change['old']) && is_array($change['old']['trigger'])) | |
| 2119 $change['old']['trigger'] = $change['old']['trigger']['value']; | |
| 2120 if (is_array($change['new']) && is_array($change['new']['trigger'])) | |
| 2121 $change['new']['trigger'] = $change['new']['trigger']['value']; | |
| 2122 } | |
| 2123 // make all property keys uppercase | |
| 2124 if ($change['property'] == 'recurrence') { | |
| 2125 $special_changes['recurrence'] = $i; | |
| 2126 foreach (array('old','new') as $m) { | |
| 2127 if (is_array($change[$m])) { | |
| 2128 $props = array(); | |
| 2129 foreach ($change[$m] as $k => $v) | |
| 2130 $props[strtoupper($k)] = $v; | |
| 2131 $change[$m] = $props; | |
| 2132 } | |
| 2133 } | |
| 2134 } | |
| 2135 // map property keys names | |
| 2136 if (is_array($prop_keymaps[$change['property']])) { | |
| 2137 foreach ($prop_keymaps[$change['property']] as $k => $dest) { | |
| 2138 if (is_array($change['old']) && array_key_exists($k, $change['old'])) { | |
| 2139 $change['old'][$dest] = $change['old'][$k]; | |
| 2140 unset($change['old'][$k]); | |
| 2141 } | |
| 2142 if (is_array($change['new']) && array_key_exists($k, $change['new'])) { | |
| 2143 $change['new'][$dest] = $change['new'][$k]; | |
| 2144 unset($change['new'][$k]); | |
| 2145 } | |
| 2146 } | |
| 2147 } | |
| 2148 | |
| 2149 if ($change['property'] == 'exdate') { | |
| 2150 $special_changes['exdate'] = $i; | |
| 2151 } | |
| 2152 else if ($change['property'] == 'rdate') { | |
| 2153 $special_changes['rdate'] = $i; | |
| 2154 } | |
| 2155 }); | |
| 2156 | |
| 2157 // merge some recurrence changes | |
| 2158 foreach (array('exdate','rdate') as $prop) { | |
| 2159 if (array_key_exists($prop, $special_changes)) { | |
| 2160 $exdate = $result['changes'][$special_changes[$prop]]; | |
| 2161 if (array_key_exists('recurrence', $special_changes)) { | |
| 2162 $recurrence = &$result['changes'][$special_changes['recurrence']]; | |
| 2163 } | |
| 2164 else { | |
| 2165 $i = count($result['changes']); | |
| 2166 $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array()); | |
| 2167 $recurrence = &$result['changes'][$i]['recurrence']; | |
| 2168 } | |
| 2169 $key = strtoupper($prop); | |
| 2170 $recurrence['old'][$key] = $exdate['old']; | |
| 2171 $recurrence['new'][$key] = $exdate['new']; | |
| 2172 unset($result['changes'][$special_changes[$prop]]); | |
| 2173 } | |
| 2174 } | |
| 2175 | |
| 2176 return $result; | |
| 2177 } | |
| 2178 | |
| 2179 return false; | |
| 2180 } | |
| 2181 | |
| 2182 /** | |
| 2183 * Return full data of a specific revision of an event | |
| 2184 * | |
| 2185 * @param array Hash array with event properties | |
| 2186 * @param mixed $rev Revision number | |
| 2187 * | |
| 2188 * @return array Event object as hash array | |
| 2189 * @see calendar_driver::get_event_revison() | |
| 2190 */ | |
| 2191 public function get_event_revison($event, $rev, $internal = false) | |
| 2192 { | |
| 2193 if (empty($this->bonnie_api)) { | |
| 2194 return false; | |
| 2195 } | |
| 2196 | |
| 2197 $eventid = $event['id']; | |
| 2198 $calid = $event['calendar']; | |
| 2199 list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event); | |
| 2200 | |
| 2201 // call Bonnie API | |
| 2202 $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid); | |
| 2203 if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { | |
| 2204 $format = kolab_format::factory('event'); | |
| 2205 $format->load($result['xml']); | |
| 2206 $event = $format->to_array(); | |
| 2207 $format->get_attachments($event, true); | |
| 2208 | |
| 2209 // get the right instance from a recurring event | |
| 2210 if ($eventid != $event['uid']) { | |
| 2211 $instance_id = substr($eventid, strlen($event['uid']) + 1); | |
| 2212 | |
| 2213 // check for recurrence exception first | |
| 2214 if ($instance = $format->get_instance($instance_id)) { | |
| 2215 $event = $instance; | |
| 2216 } | |
| 2217 else { | |
| 2218 // not a exception, compute recurrence... | |
| 2219 $event['_formatobj'] = $format; | |
| 2220 $recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone()); | |
| 2221 foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) { | |
| 2222 if ($instance['id'] == $eventid) { | |
| 2223 $event = $instance; | |
| 2224 break; | |
| 2225 } | |
| 2226 } | |
| 2227 } | |
| 2228 } | |
| 2229 | |
| 2230 if ($format->is_valid()) { | |
| 2231 $event['calendar'] = $calid; | |
| 2232 $event['rev'] = $result['rev']; | |
| 2233 return $internal ? $event : self::to_rcube_event($event); | |
| 2234 } | |
| 2235 } | |
| 2236 | |
| 2237 return false; | |
| 2238 } | |
| 2239 | |
| 2240 /** | |
| 2241 * Command the backend to restore a certain revision of an event. | |
| 2242 * This shall replace the current event with an older version. | |
| 2243 * | |
| 2244 * @param mixed UID string or hash array with event properties: | |
| 2245 * id: Event identifier | |
| 2246 * calendar: Calendar identifier | |
| 2247 * @param mixed $rev Revision number | |
| 2248 * | |
| 2249 * @return boolean True on success, False on failure | |
| 2250 */ | |
| 2251 public function restore_event_revision($event, $rev) | |
| 2252 { | |
| 2253 if (empty($this->bonnie_api)) { | |
| 2254 return false; | |
| 2255 } | |
| 2256 | |
| 2257 list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event); | |
| 2258 $calendar = $this->get_calendar($event['calendar']); | |
| 2259 $success = false; | |
| 2260 | |
| 2261 if ($calendar && $calendar->storage && $calendar->editable) { | |
| 2262 if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) { | |
| 2263 $imap = $this->rc->get_storage(); | |
| 2264 | |
| 2265 // insert $raw_msg as new message | |
| 2266 if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) { | |
| 2267 $success = true; | |
| 2268 | |
| 2269 // delete old revision from imap and cache | |
| 2270 $imap->delete_message($msguid, $calendar->storage->name); | |
| 2271 $calendar->storage->cache->set($msguid, false); | |
| 2272 } | |
| 2273 } | |
| 2274 } | |
| 2275 | |
| 2276 return $success; | |
| 2277 } | |
| 2278 | |
| 2279 /** | |
| 2280 * Helper method to resolved the given event identifier into uid and folder | |
| 2281 * | |
| 2282 * @return array (uid,folder,msguid) tuple | |
| 2283 */ | |
| 2284 private function _resolve_event_identity($event) | |
| 2285 { | |
| 2286 $mailbox = $msguid = null; | |
| 2287 if (is_array($event)) { | |
| 2288 $uid = $event['uid'] ?: $event['id']; | |
| 2289 if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) { | |
| 2290 $mailbox = $cal->get_mailbox_id(); | |
| 2291 | |
| 2292 // get event object from storage in order to get the real object uid an msguid | |
| 2293 if ($ev = $cal->get_event($event['id'])) { | |
| 2294 $msguid = $ev['_msguid']; | |
| 2295 $uid = $ev['uid']; | |
| 2296 } | |
| 2297 } | |
| 2298 } | |
| 2299 else { | |
| 2300 $uid = $event; | |
| 2301 | |
| 2302 // get event object from storage in order to get the real object uid an msguid | |
| 2303 if ($ev = $this->get_event($event)) { | |
| 2304 $mailbox = $ev['_mailbox']; | |
| 2305 $msguid = $ev['_msguid']; | |
| 2306 $uid = $ev['uid']; | |
| 2307 } | |
| 2308 } | |
| 2309 | |
| 2310 return array($uid, $mailbox, $msguid); | |
| 2311 } | |
| 2312 | |
| 2313 /** | |
| 2314 * Callback function to produce driver-specific calendar create/edit form | |
| 2315 * | |
| 2316 * @param string Request action 'form-edit|form-new' | |
| 2317 * @param array Calendar properties (e.g. id, color) | |
| 2318 * @param array Edit form fields | |
| 2319 * | |
| 2320 * @return string HTML content of the form | |
| 2321 */ | |
| 2322 public function calendar_form($action, $calendar, $formfields) | |
| 2323 { | |
| 2324 // show default dialog for birthday calendar | |
| 2325 if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) { | |
| 2326 if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID) | |
| 2327 unset($formfields['showalarms']); | |
| 2328 return parent::calendar_form($action, $calendar, $formfields); | |
| 2329 } | |
| 2330 | |
| 2331 $this->_read_calendars(); | |
| 2332 | |
| 2333 if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) { | |
| 2334 $folder = $cal->get_realname(); // UTF7 | |
| 2335 $color = $cal->get_color(); | |
| 2336 } | |
| 2337 else { | |
| 2338 $folder = ''; | |
| 2339 $color = ''; | |
| 2340 } | |
| 2341 | |
| 2342 $hidden_fields[] = array('name' => 'oldname', 'value' => $folder); | |
| 2343 | |
| 2344 $storage = $this->rc->get_storage(); | |
| 2345 $delim = $storage->get_hierarchy_delimiter(); | |
| 2346 $form = array(); | |
| 2347 | |
| 2348 if (strlen($folder)) { | |
| 2349 $path_imap = explode($delim, $folder); | |
| 2350 array_pop($path_imap); // pop off name part | |
| 2351 $path_imap = implode($path_imap, $delim); | |
| 2352 | |
| 2353 $options = $storage->folder_info($folder); | |
| 2354 } | |
| 2355 else { | |
| 2356 $path_imap = ''; | |
| 2357 } | |
| 2358 | |
| 2359 // General tab | |
| 2360 $form['props'] = array( | |
| 2361 'name' => $this->rc->gettext('properties'), | |
| 2362 ); | |
| 2363 | |
| 2364 // Disable folder name input | |
| 2365 if (!empty($options) && ($options['norename'] || $options['protected'])) { | |
| 2366 $input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name')); | |
| 2367 $formfields['name']['value'] = kolab_storage::object_name($folder) | |
| 2368 . $input_name->show($folder); | |
| 2369 } | |
| 2370 | |
| 2371 // calendar name (default field) | |
| 2372 $form['props']['fieldsets']['location'] = array( | |
| 2373 'name' => $this->rc->gettext('location'), | |
| 2374 'content' => array( | |
| 2375 'name' => $formfields['name'] | |
| 2376 ), | |
| 2377 ); | |
| 2378 | |
| 2379 if (!empty($options) && ($options['norename'] || $options['protected'])) { | |
| 2380 // prevent user from moving folder | |
| 2381 $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap); | |
| 2382 } | |
| 2383 else { | |
| 2384 $select = kolab_storage::folder_selector('event', array('name' => 'parent', 'id' => 'calendar-parent'), $folder); | |
| 2385 $form['props']['fieldsets']['location']['content']['path'] = array( | |
| 2386 'id' => 'calendar-parent', | |
| 2387 'label' => $this->cal->gettext('parentcalendar'), | |
| 2388 'value' => $select->show(strlen($folder) ? $path_imap : ''), | |
| 2389 ); | |
| 2390 } | |
| 2391 | |
| 2392 // calendar color (default field) | |
| 2393 $form['props']['fieldsets']['settings'] = array( | |
| 2394 'name' => $this->rc->gettext('settings'), | |
| 2395 'content' => array( | |
| 2396 'color' => $formfields['color'], | |
| 2397 'showalarms' => $formfields['showalarms'], | |
| 2398 ), | |
| 2399 ); | |
| 2400 | |
| 2401 | |
| 2402 if ($action != 'form-new') { | |
| 2403 $form['sharing'] = array( | |
| 2404 'name' => rcube::Q($this->cal->gettext('tabsharing')), | |
| 2405 'content' => html::tag('iframe', array( | |
| 2406 'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)), | |
| 2407 'width' => '100%', | |
| 2408 'height' => 350, | |
| 2409 'border' => 0, | |
| 2410 'style' => 'border:0'), | |
| 2411 ''), | |
| 2412 ); | |
| 2413 } | |
| 2414 | |
| 2415 $this->form_html = ''; | |
| 2416 if (is_array($hidden_fields)) { | |
| 2417 foreach ($hidden_fields as $field) { | |
| 2418 $hiddenfield = new html_hiddenfield($field); | |
| 2419 $this->form_html .= $hiddenfield->show() . "\n"; | |
| 2420 } | |
| 2421 } | |
| 2422 | |
| 2423 // Create form output | |
| 2424 foreach ($form as $tab) { | |
| 2425 if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) { | |
| 2426 $content = ''; | |
| 2427 foreach ($tab['fieldsets'] as $fieldset) { | |
| 2428 $subcontent = $this->get_form_part($fieldset); | |
| 2429 if ($subcontent) { | |
| 2430 $content .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($fieldset['name'])) . $subcontent) ."\n"; | |
| 2431 } | |
| 2432 } | |
| 2433 } | |
| 2434 else { | |
| 2435 $content = $this->get_form_part($tab); | |
| 2436 } | |
| 2437 | |
| 2438 if ($content) { | |
| 2439 $this->form_html .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) ."\n"; | |
| 2440 } | |
| 2441 } | |
| 2442 | |
| 2443 // Parse form template for skin-dependent stuff | |
| 2444 $this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html')); | |
| 2445 return $this->rc->output->parse('calendar.kolabform', false, false); | |
| 2446 } | |
| 2447 | |
| 2448 /** | |
| 2449 * Handler for template object | |
| 2450 */ | |
| 2451 public function calendar_form_html() | |
| 2452 { | |
| 2453 return $this->form_html; | |
| 2454 } | |
| 2455 | |
| 2456 /** | |
| 2457 * Helper function used in calendar_form_content(). Creates a part of the form. | |
| 2458 */ | |
| 2459 private function get_form_part($form) | |
| 2460 { | |
| 2461 $content = ''; | |
| 2462 | |
| 2463 if (is_array($form['content']) && !empty($form['content'])) { | |
| 2464 $table = new html_table(array('cols' => 2)); | |
| 2465 foreach ($form['content'] as $col => $colprop) { | |
| 2466 $label = !empty($colprop['label']) ? $colprop['label'] : $this->cal->gettext($col); | |
| 2467 | |
| 2468 $table->add('title', html::label($colprop['id'], rcube::Q($label))); | |
| 2469 $table->add(null, $colprop['value']); | |
| 2470 } | |
| 2471 $content = $table->show(); | |
| 2472 } | |
| 2473 else { | |
| 2474 $content = $form['content']; | |
| 2475 } | |
| 2476 | |
| 2477 return $content; | |
| 2478 } | |
| 2479 | |
| 2480 | |
| 2481 /** | |
| 2482 * Handler to render ACL form for a calendar folder | |
| 2483 */ | |
| 2484 public function calendar_acl() | |
| 2485 { | |
| 2486 $this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form')); | |
| 2487 $this->rc->output->send('calendar.kolabacl'); | |
| 2488 } | |
| 2489 | |
| 2490 /** | |
| 2491 * Handler for ACL form template object | |
| 2492 */ | |
| 2493 public function calendar_acl_form() | |
| 2494 { | |
| 2495 $calid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); | |
| 2496 if ($calid && ($cal = $this->get_calendar($calid))) { | |
| 2497 $folder = $cal->get_realname(); // UTF7 | |
| 2498 $color = $cal->get_color(); | |
| 2499 } | |
| 2500 else { | |
| 2501 $folder = ''; | |
| 2502 $color = ''; | |
| 2503 } | |
| 2504 | |
| 2505 $storage = $this->rc->get_storage(); | |
| 2506 $delim = $storage->get_hierarchy_delimiter(); | |
| 2507 $form = array(); | |
| 2508 | |
| 2509 if (strlen($folder)) { | |
| 2510 $path_imap = explode($delim, $folder); | |
| 2511 array_pop($path_imap); // pop off name part | |
| 2512 $path_imap = implode($path_imap, $delim); | |
| 2513 | |
| 2514 $options = $storage->folder_info($folder); | |
| 2515 | |
| 2516 // Allow plugins to modify the form content (e.g. with ACL form) | |
| 2517 $plugin = $this->rc->plugins->exec_hook('calendar_form_kolab', | |
| 2518 array('form' => $form, 'options' => $options, 'name' => $folder)); | |
| 2519 } | |
| 2520 | |
| 2521 if (!$plugin['form']['sharing']['content']) | |
| 2522 $plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights')); | |
| 2523 | |
| 2524 return $plugin['form']['sharing']['content']; | |
| 2525 } | |
| 2526 | |
| 2527 /** | |
| 2528 * Handler for user_delete plugin hook | |
| 2529 */ | |
| 2530 public function user_delete($args) | |
| 2531 { | |
| 2532 $db = $this->rc->get_dbh(); | |
| 2533 foreach (array('kolab_alarms', 'itipinvitations') as $table) { | |
| 2534 $db->query("DELETE FROM " . $this->rc->db->table_name($table, true) | |
| 2535 . " WHERE `user_id` = ?", $args['user']->ID); | |
| 2536 } | |
| 2537 } | |
| 2538 } |
