1<?php 2 3/** 4 * Kolab calendar storage class simulating a virtual user calendar 5 * 6 * @author Thomas Bruederli <bruederli@kolabsys.com> 7 * 8 * Copyright (C) 2014-2016, Kolab Systems AG <contact@kolabsys.com> 9 * 10 * This program is free software: you can redistribute it and/or modify 11 * it under the terms of the GNU Affero General Public License as 12 * published by the Free Software Foundation, either version 3 of the 13 * License, or (at your option) any later version. 14 * 15 * This program is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU Affero General Public License for more details. 19 * 20 * You should have received a copy of the GNU Affero General Public License 21 * along with this program. If not, see <http://www.gnu.org/licenses/>. 22 */ 23 24class kolab_user_calendar extends kolab_calendar 25{ 26 public $id = 'unknown'; 27 public $ready = false; 28 public $editable = false; 29 public $attachments = false; 30 public $subscriptions = false; 31 32 protected $userdata = array(); 33 protected $timeindex = array(); 34 35 36 /** 37 * Default constructor 38 */ 39 public function __construct($user_or_folder, $calendar) 40 { 41 $this->cal = $calendar; 42 $this->imap = $calendar->rc->get_storage(); 43 44 // full user record is provided 45 if (is_array($user_or_folder)) { 46 $this->userdata = $user_or_folder; 47 $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata); 48 } 49 else if ($user_or_folder instanceof kolab_storage_folder_user) { 50 $this->storage = $user_or_folder; 51 $this->userdata = $this->storage->ldaprec; 52 } 53 else { // get user record from LDAP 54 $this->storage = new kolab_storage_folder_user($user_or_folder); 55 $this->userdata = $this->storage->ldaprec; 56 } 57 58 $this->ready = !empty($this->userdata['kolabtargetfolder']); 59 $this->storage->type = 'event'; 60 61 if ($this->ready) { 62 // ID is derrived from the user's kolabtargetfolder attribute 63 $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true); 64 $this->imap_folder = $this->userdata['kolabtargetfolder']; 65 $this->name = $this->storage->name; 66 $this->parent = ''; // user calendars are top level 67 68 // user-specific alarms settings win 69 $prefs = $this->cal->rc->config->get('kolab_calendars', array()); 70 if (isset($prefs[$this->id]['showalarms'])) 71 $this->alarms = $prefs[$this->id]['showalarms']; 72 } 73 } 74 75 /** 76 * Getter for a nice and human readable name for this calendar 77 * 78 * @return string Name of this calendar 79 */ 80 public function get_name() 81 { 82 return $this->userdata['displayname'] ?: ($this->userdata['name'] ?: $this->userdata['mail']); 83 } 84 85 /** 86 * Getter for the IMAP folder owner 87 * 88 * @param bool Return a fully qualified owner name (unused) 89 * 90 * @return string Name of the folder owner 91 */ 92 public function get_owner($fully_qualified = false) 93 { 94 return $this->userdata['mail']; 95 } 96 97 /** 98 * 99 */ 100 public function get_title() 101 { 102 return trim($this->userdata['displayname'] . '; ' . $this->userdata['mail'], '; '); 103 } 104 105 /** 106 * Getter for the name of the namespace to which the IMAP folder belongs 107 * 108 * @return string Name of the namespace (personal, other, shared) 109 */ 110 public function get_namespace() 111 { 112 return 'other user'; 113 } 114 115 /** 116 * Getter for the top-end calendar folder name (not the entire path) 117 * 118 * @return string Name of this calendar 119 */ 120 public function get_foldername() 121 { 122 return $this->get_name(); 123 } 124 125 /** 126 * Return color to display this calendar 127 */ 128 public function get_color($default = null) 129 { 130 // calendar color is stored in local user prefs 131 $prefs = $this->cal->rc->config->get('kolab_calendars', array()); 132 133 if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) 134 return $prefs[$this->id]['color']; 135 136 return $default ?: 'cc0000'; 137 } 138 139 /** 140 * Compose an URL for CalDAV access to this calendar (if configured) 141 */ 142 public function get_caldav_url() 143 { 144 return false; 145 } 146 147 /** 148 * Check subscription status of this folder 149 * 150 * @return boolean True if subscribed, false if not 151 */ 152 public function is_subscribed() 153 { 154 return $this->storage->is_subscribed(); 155 } 156 157 /** 158 * Update properties of this calendar folder 159 * 160 * @see calendar_driver::edit_calendar() 161 */ 162 public function update(&$prop) 163 { 164 // don't change anything. 165 // let kolab_driver save props in local prefs 166 return $prop['id']; 167 } 168 169 /** 170 * Getter for a single event object 171 */ 172 public function get_event($id) 173 { 174 // TODO: implement this 175 return $this->events[$id]; 176 } 177 178 /** 179 * Get attachment body 180 * @see calendar_driver::get_attachment_body() 181 */ 182 public function get_attachment_body($id, $event) 183 { 184 if (!$event['calendar'] && ($ev = $this->get_event($event['id']))) { 185 $event['calendar'] = $ev['calendar']; 186 } 187 188 if ($event['calendar'] && ($cal = $this->cal->get_calendar($event['calendar']))) { 189 return $cal->get_attachment_body($id, $event); 190 } 191 192 return false; 193 } 194 195 /** 196 * @param integer Event's new start (unix timestamp) 197 * @param integer Event's new end (unix timestamp) 198 * @param string Search query (optional) 199 * @param boolean Include virtual events (optional) 200 * @param array Additional parameters to query storage 201 * @param array Additional query to filter events 202 * 203 * @return array A list of event records 204 */ 205 public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null) 206 { 207 // convert to DateTime for comparisons 208 try { 209 $start_dt = new DateTime('@'.$start); 210 } 211 catch (Exception $e) { 212 $start_dt = new DateTime('@0'); 213 } 214 try { 215 $end_dt = new DateTime('@'.$end); 216 } 217 catch (Exception $e) { 218 $end_dt = new DateTime('today +10 years'); 219 } 220 221 $limit_changed = null; 222 if (!empty($query)) { 223 foreach ($query as $q) { 224 if ($q[0] == 'changed' && $q[1] == '>=') { 225 try { $limit_changed = new DateTime('@'.$q[2]); } 226 catch (Exception $e) { /* ignore */ } 227 } 228 } 229 } 230 231 // aggregate all calendar folders the user shares (but are not activated) 232 foreach (kolab_storage::list_user_folders($this->userdata, 'event', 2) as $foldername) { 233 $cal = new kolab_calendar($foldername, $this->cal); 234 foreach ($cal->list_events($start, $end, $search, 1) as $event) { 235 $uid = $event['id'] ?: $event['uid']; 236 $this->events[$uid] = $event; 237 $this->timeindex[$this->time_key($event)] = $uid; 238 } 239 } 240 241 // get events from the user's free/busy feed (for quickview only) 242 $fbview = $this->cal->rc->config->get('calendar_include_freebusy_data', 1); 243 if ($fbview && ($fbview == 1 || !empty($_REQUEST['_quickview'])) && empty($search)) { 244 $this->fetch_freebusy($limit_changed); 245 } 246 247 $events = array(); 248 foreach ($this->events as $event) { 249 // list events in requested time window 250 if ($event['start'] <= $end_dt && $event['end'] >= $start_dt && 251 (!$limit_changed || !$event['changed'] || $event['changed'] >= $limit_changed)) { 252 $events[] = $event; 253 } 254 } 255 256 // avoid session race conditions that will loose temporary subscriptions 257 $this->cal->rc->session->nowrite = true; 258 259 return $events; 260 } 261 262 /** 263 * Get number of events in the given calendar 264 * 265 * @param integer Date range start (unix timestamp) 266 * @param integer Date range end (unix timestamp) 267 * @param array Additional query to filter events 268 * 269 * @return integer Count 270 */ 271 public function count_events($start, $end = null, $filter_query = null) 272 { 273 // not implemented 274 return 0; 275 } 276 277 /** 278 * Helper method to fetch free/busy data for the user and turn it into calendar data 279 */ 280 private function fetch_freebusy($limit_changed = null) 281 { 282 // ask kolab server first 283 try { 284 $request_config = array( 285 'store_body' => true, 286 'follow_redirects' => true, 287 ); 288 $request = libkolab::http_request(kolab_storage::get_freebusy_url($this->userdata['mail']), 'GET', $request_config); 289 $response = $request->send(); 290 291 // authentication required 292 if ($response->getStatus() == 401) { 293 $request->setAuth($this->cal->rc->user->get_username(), $this->cal->rc->decrypt($_SESSION['password'])); 294 $response = $request->send(); 295 } 296 297 if ($response->getStatus() == 200) 298 $fbdata = $response->getBody(); 299 300 unset($request, $response); 301 } 302 catch (Exception $e) { 303 rcube::raise_error(array( 304 'code' => 900, 305 'type' => 'php', 306 'file' => __FILE__, 307 'line' => __LINE__, 308 'message' => "Error fetching free/busy information: " . $e->getMessage()), 309 true, false); 310 311 return false; 312 } 313 314 $statusmap = array( 315 'FREE' => 'free', 316 'BUSY' => 'busy', 317 'BUSY-TENTATIVE' => 'tentative', 318 'X-OUT-OF-OFFICE' => 'outofoffice', 319 'OOF' => 'outofoffice', 320 ); 321 $titlemap = array( 322 'FREE' => $this->cal->gettext('availfree'), 323 'BUSY' => $this->cal->gettext('availbusy'), 324 'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'), 325 'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'), 326 ); 327 328 // rcube::console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata); 329 330 // parse free-busy information 331 $count = 0; 332 if ($fbdata) { 333 $ical = $this->cal->get_ical(); 334 $ical->import($fbdata); 335 if ($fb = $ical->freebusy) { 336 // consider 'changed >= X' queries 337 if ($limit_changed && $fb['created'] && $fb['created'] < $limit_changed) { 338 return 0; 339 } 340 341 foreach ($fb['periods'] as $tuple) { 342 list($from, $to, $type) = $tuple; 343 $event = array( 344 'uid' => md5($this->id . $from->format('U') . '/' . $to->format('U')), 345 'calendar' => $this->id, 346 'changed' => $fb['created'] ?: new DateTime(), 347 'title' => $this->get_name() . ' ' . ($titlemap[$type] ?: $type), 348 'start' => $from, 349 'end' => $to, 350 'free_busy' => $statusmap[$type] ?: 'busy', 351 'className' => 'fc-type-freebusy', 352 'organizer' => array( 353 'email' => $this->userdata['mail'], 354 'name' => $this->userdata['displayname'], 355 ), 356 ); 357 358 // avoid duplicate entries 359 $key = $this->time_key($event); 360 if (!$this->timeindex[$key]) { 361 $this->events[$event['uid']] = $event; 362 $this->timeindex[$key] = $event['uid']; 363 $count++; 364 } 365 } 366 } 367 } 368 369 return $count; 370 } 371 372 /** 373 * Helper to build a key for the absolute time slot the given event convers 374 */ 375 private function time_key($event) 376 { 377 return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']) ? $event['end']->format('U') : '0'); 378 } 379 380 /** 381 * Create a new event record 382 * 383 * @see calendar_driver::new_event() 384 * 385 * @return mixed The created record ID on success, False on error 386 */ 387 public function insert_event($event) 388 { 389 return false; 390 } 391 392 /** 393 * Update a specific event record 394 * 395 * @see calendar_driver::new_event() 396 * @return boolean True on success, False on error 397 */ 398 public function update_event($event, $exception_id = null) 399 { 400 return false; 401 } 402 403 /** 404 * Delete an event record 405 * 406 * @see calendar_driver::remove_event() 407 * @return boolean True on success, False on error 408 */ 409 public function delete_event($event, $force = true) 410 { 411 return false; 412 } 413 414 /** 415 * Restore deleted event record 416 * 417 * @see calendar_driver::undelete_event() 418 * @return boolean True on success, False on error 419 */ 420 public function restore_event($event) 421 { 422 return false; 423 } 424} 425