1<?php 2/** 3 * Helper Class for the DAVCal plugin 4 * This helper does the actual work. 5 * 6 */ 7 8// must be run within Dokuwiki 9if(!defined('DOKU_INC')) die(); 10 11class helper_plugin_davcal extends DokuWiki_Plugin { 12 13 protected $sqlite = null; 14 protected $cachedValues = array(); 15 16 /** 17 * Constructor to load the configuration and the SQLite plugin 18 */ 19 public function helper_plugin_davcal() { 20 $this->sqlite =& plugin_load('helper', 'sqlite'); 21 global $conf; 22 dbglog('---- DAVCAL helper.php init'); 23 if(!$this->sqlite) 24 { 25 dbglog('This plugin requires the sqlite plugin. Please install it.'); 26 msg('This plugin requires the sqlite plugin. Please install it.'); 27 return; 28 } 29 30 if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/')) 31 { 32 dbglog('Error initialising the SQLite DB for DAVCal'); 33 return; 34 } 35 } 36 37 /** 38 * Retrieve meta data for a given page 39 * 40 * @param string $id optional The page ID 41 * @return array The metadata 42 */ 43 private function getMeta($id = null) { 44 global $ID; 45 global $INFO; 46 47 if ($id === null) $id = $ID; 48 49 if($ID === $id && $INFO['meta']) { 50 $meta = $INFO['meta']; 51 } else { 52 $meta = p_get_metadata($id); 53 } 54 55 return $meta; 56 } 57 58 /** 59 * Retrieve the meta data for a given page 60 * 61 * @param string $id optional The page ID 62 * @return array with meta data 63 */ 64 public function getCalendarMetaForPage($id = null) 65 { 66 if(is_null($id)) 67 { 68 global $ID; 69 $id = $ID; 70 } 71 72 $meta = $this->getMeta($id); 73 if(isset($meta['plugin_davcal'])) 74 return $meta['plugin_davcal']; 75 else 76 return array(); 77 } 78 79 /** 80 * Check the permission of a user for a given calendar ID 81 * 82 * @param string $id The calendar ID to check 83 * @return int AUTH_* constants 84 */ 85 public function checkCalendarPermission($id) 86 { 87 if(strpos($page, 'webdav://') === 0) 88 { 89 $wdc =& plugin_load('helper', 'webdavclient'); 90 if(is_null($wdc)) 91 return AUTH_NONE; 92 $connectionId = str_replace('webdav://', '', $page); 93 $settings = $wdc->getConnection($connectionId); 94 if($settings === false) 95 return AUTH_NONE; 96 if($settings['write'] === '1') 97 return AUTH_CREATE; 98 return AUTH_READ; 99 } 100 else 101 { 102 $calid = $this->getCalendarIdForPage($id); 103 // We return AUTH_READ if the calendar does not exist. This makes 104 // davcal happy when there are just included calendars 105 if($calid === false) 106 return AUTH_READ; 107 return auth_quickaclcheck($id); 108 } 109 } 110 111 /** 112 * Filter calendar pages and return only those where the current 113 * user has at least read permission. 114 * 115 * @param array $calendarPages Array with calendar pages to check 116 * @return array with filtered calendar pages 117 */ 118 public function filterCalendarPagesByUserPermission($calendarPages) 119 { 120 $retList = array(); 121 foreach($calendarPages as $page => $data) 122 { 123 // WebDAV Connections are always readable 124 if(strpos($page, 'webdav://') === 0) 125 { 126 $retList[$page] = $data; 127 } 128 elseif(auth_quickaclcheck($page) >= AUTH_READ) 129 { 130 $retList[$page] = $data; 131 } 132 } 133 return $retList; 134 } 135 136 /** 137 * Get all calendar pages used by a given page 138 * based on the stored metadata 139 * 140 * @param string $id optional The page id 141 * @return mixed The pages as array or false 142 */ 143 public function getCalendarPagesByMeta($id = null) 144 { 145 if(is_null($id)) 146 { 147 global $ID; 148 $id = $ID; 149 } 150 151 $meta = $this->getCalendarMetaForPage($id); 152 153 if(isset($meta['id'])) 154 { 155 // Filter the list of pages by permission 156 $pages = $this->filterCalendarPagesByUserPermission($meta['id']); 157 if(empty($pages)) 158 return false; 159 return $pages; 160 } 161 return false; 162 } 163 164 /** 165 * Get a list of calendar names/pages/ids/colors 166 * for an array of page ids 167 * 168 * @param array $calendarPages The calendar pages to retrieve 169 * @return array The list 170 */ 171 public function getCalendarMapForIDs($calendarPages) 172 { 173 $data = array(); 174 foreach($calendarPages as $page => $color) 175 { 176 if(strpos($page, 'webdav://') === 0) 177 { 178 $wdc =& plugin_load('helper', 'webdavclient'); 179 if(is_null($wdc)) 180 continue; 181 $connectionId = str_replace('webdav://', '', $page); 182 $settings = $wdc->getConnection($connectionId); 183 if($settings === false) 184 continue; 185 $name = $settings['displayname']; 186 $write = ($settings['write'] === '1'); 187 $calid = $connectionId; 188 $color = '#3a87ad'; 189 } 190 else 191 { 192 $calid = $this->getCalendarIdForPage($page); 193 if($calid !== false) 194 { 195 $settings = $this->getCalendarSettings($calid); 196 $name = $settings['displayname']; 197 $color = $settings['calendarcolor']; 198 $write = (auth_quickaclcheck($page) > AUTH_READ); 199 } 200 else 201 { 202 continue; 203 } 204 } 205 $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid, 206 'color' => $color, 'write' => $write); 207 } 208 return $data; 209 } 210 211 /** 212 * Get the saved calendar color for a given page. 213 * 214 * @param string $id optional The page ID 215 * @return mixed The color on success, otherwise false 216 */ 217 public function getCalendarColorForPage($id = null) 218 { 219 if(is_null($id)) 220 { 221 global $ID; 222 $id = $ID; 223 } 224 225 $calid = $this->getCalendarIdForPage($id); 226 if($calid === false) 227 return false; 228 229 return $this->getCalendarColorForCalendar($calid); 230 } 231 232 /** 233 * Get the saved calendar color for a given calendar ID. 234 * 235 * @param string $id optional The calendar ID 236 * @return mixed The color on success, otherwise false 237 */ 238 public function getCalendarColorForCalendar($calid) 239 { 240 if(isset($this->cachedValues['calendarcolor'][$calid])) 241 return $this->cachedValues['calendarcolor'][$calid]; 242 243 $row = $this->getCalendarSettings($calid); 244 245 if(!isset($row['calendarcolor'])) 246 return false; 247 248 $color = $row['calendarcolor']; 249 $this->cachedValues['calendarcolor'][$calid] = $color; 250 return $color; 251 } 252 253 /** 254 * Get the user's principal URL for iOS sync 255 * @param string $user the user name 256 * @return the URL to the principal sync 257 */ 258 public function getPrincipalUrlForUser($user) 259 { 260 if(is_null($user)) 261 return false; 262 $url = DOKU_URL.'lib/plugins/davcal/calendarserver.php/principals/'.$user; 263 return $url; 264 } 265 266 /** 267 * Set the calendar color for a given page. 268 * 269 * @param string $color The color definition 270 * @param string $id optional The page ID 271 * @return boolean True on success, otherwise false 272 */ 273 public function setCalendarColorForPage($color, $id = null) 274 { 275 if(is_null($id)) 276 { 277 global $ID; 278 $id = $ID; 279 } 280 $calid = $this->getCalendarIdForPage($id); 281 if($calid === false) 282 return false; 283 284 $query = "UPDATE calendars SET calendarcolor = ? ". 285 " WHERE id = ?"; 286 $res = $this->sqlite->query($query, $color, $calid); 287 if($res !== false) 288 { 289 $this->cachedValues['calendarcolor'][$calid] = $color; 290 return true; 291 } 292 return false; 293 } 294 295 /** 296 * Set the calendar name and description for a given page with a given 297 * page id. 298 * If the calendar doesn't exist, the calendar is created! 299 * 300 * @param string $name The name of the new calendar 301 * @param string $description The description of the new calendar 302 * @param string $id (optional) The ID of the page 303 * @param string $userid The userid of the creating user 304 * 305 * @return boolean True on success, otherwise false. 306 */ 307 public function setCalendarNameForPage($name, $description, $id = null, $userid = null) 308 { 309 if(is_null($id)) 310 { 311 global $ID; 312 $id = $ID; 313 } 314 if(is_null($userid)) 315 { 316 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 317 { 318 $userid = $_SERVER['REMOTE_USER']; 319 } 320 else 321 { 322 $userid = uniqid('davcal-'); 323 } 324 } 325 $calid = $this->getCalendarIdForPage($id); 326 if($calid === false) 327 return $this->createCalendarForPage($name, $description, $id, $userid); 328 329 $query = "UPDATE calendars SET displayname = ?, description = ? WHERE id = ?"; 330 $res = $this->sqlite->query($query, $name, $description, $calid); 331 if($res !== false) 332 return true; 333 return false; 334 } 335 336 /** 337 * Update a calendar's displayname 338 * 339 * @param int $calid The calendar's ID 340 * @param string $name The new calendar name 341 * 342 * @return boolean True on success, otherwise false 343 */ 344 public function updateCalendarName($calid, $name) 345 { 346 $query = "UPDATE calendars SET displayname = ? WHERE id = ?"; 347 $res = $this->sqlite->query($query, $calid, $name); 348 if($res !== false) 349 { 350 $this->updateSyncTokenLog($calid, '', 'modified'); 351 return true; 352 } 353 return false; 354 } 355 356 /** 357 * Update the calendar description 358 * 359 * @param int $calid The calendar's ID 360 * @param string $description The new calendar's description 361 * 362 * @return boolean True on success, otherwise false 363 */ 364 public function updateCalendarDescription($calid, $description) 365 { 366 $query = "UPDATE calendars SET description = ? WHERE id = ?"; 367 $res = $this->sqlite->query($query, $calid, $description); 368 if($res !== false) 369 { 370 $this->updateSyncTokenLog($calid, '', 'modified'); 371 return true; 372 } 373 return false; 374 } 375 376 /** 377 * Update a calendar's timezone information 378 * 379 * @param int $calid The calendar's ID 380 * @param string $timezone The new timezone to set 381 * 382 * @return boolean True on success, otherwise false 383 */ 384 public function updateCalendarTimezone($calid, $timezone) 385 { 386 $query = "UPDATE calendars SET timezone = ? WHERE id = ?"; 387 $res = $this->sqlite->query($query, $calid, $timezone); 388 if($res !== false) 389 { 390 $this->updateSyncTokenLog($calid, '', 'modified'); 391 return true; 392 } 393 return false; 394 } 395 396 /** 397 * Save the personal settings to the SQLite database 'calendarsettings'. 398 * 399 * @param array $settings The settings array to store 400 * @param string $userid (optional) The userid to store 401 * 402 * @param boolean True on success, otherwise false 403 */ 404 public function savePersonalSettings($settings, $userid = null) 405 { 406 if(is_null($userid)) 407 { 408 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 409 { 410 $userid = $_SERVER['REMOTE_USER']; 411 } 412 else 413 { 414 return false; 415 } 416 } 417 $this->sqlite->query("BEGIN TRANSACTION"); 418 419 $query = "DELETE FROM calendarsettings WHERE userid = ?"; 420 $this->sqlite->query($query, $userid); 421 422 foreach($settings as $key => $value) 423 { 424 $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)"; 425 $res = $this->sqlite->query($query, $userid, $key, $value); 426 if($res === false) 427 return false; 428 } 429 $this->sqlite->query("COMMIT TRANSACTION"); 430 $this->cachedValues['settings'][$userid] = $settings; 431 return true; 432 } 433 434 /** 435 * Retrieve the settings array for a given user id. 436 * Some sane defaults are returned, currently: 437 * 438 * timezone => local 439 * weeknumbers => 0 440 * workweek => 0 441 * 442 * @param string $userid (optional) The user id to retrieve 443 * 444 * @return array The settings array 445 */ 446 public function getPersonalSettings($userid = null) 447 { 448 // Some sane default settings 449 $settings = array( 450 'timezone' => $this->getConf('timezone'), 451 'weeknumbers' => $this->getConf('weeknumbers'), 452 'workweek' => $this->getConf('workweek'), 453 'monday' => $this->getConf('monday'), 454 'timeformat' => $this->getConf('timeformat') 455 ); 456 if(is_null($userid)) 457 { 458 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 459 { 460 $userid = $_SERVER['REMOTE_USER']; 461 } 462 else 463 { 464 return $settings; 465 } 466 } 467 468 if(isset($this->cachedValues['settings'][$userid])) 469 return $this->cachedValues['settings'][$userid]; 470 $query = "SELECT key, value FROM calendarsettings WHERE userid = ?"; 471 $res = $this->sqlite->query($query, $userid); 472 $arr = $this->sqlite->res2arr($res); 473 foreach($arr as $row) 474 { 475 $settings[$row['key']] = $row['value']; 476 } 477 $this->cachedValues['settings'][$userid] = $settings; 478 return $settings; 479 } 480 481 /** 482 * Retrieve the calendar ID based on a page ID from the SQLite table 483 * 'pagetocalendarmapping'. 484 * 485 * @param string $id (optional) The page ID to retrieve the corresponding calendar 486 * 487 * @return mixed the ID on success, otherwise false 488 */ 489 public function getCalendarIdForPage($id = null) 490 { 491 if(is_null($id)) 492 { 493 global $ID; 494 $id = $ID; 495 } 496 497 if(isset($this->cachedValues['calid'][$id])) 498 return $this->cachedValues['calid'][$id]; 499 500 $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?"; 501 $res = $this->sqlite->query($query, $id); 502 $row = $this->sqlite->res2row($res); 503 if(isset($row['calid'])) 504 { 505 $calid = $row['calid']; 506 $this->cachedValues['calid'] = $calid; 507 return $calid; 508 } 509 return false; 510 } 511 512 /** 513 * Retrieve the complete calendar id to page mapping. 514 * This is necessary to be able to retrieve a list of 515 * calendars for a given user and check the access rights. 516 * 517 * @return array The mapping array 518 */ 519 public function getCalendarIdToPageMapping() 520 { 521 $query = "SELECT calid, page FROM pagetocalendarmapping"; 522 $res = $this->sqlite->query($query); 523 $arr = $this->sqlite->res2arr($res); 524 return $arr; 525 } 526 527 /** 528 * Retrieve all calendar IDs a given user has access to. 529 * The user is specified by the principalUri, so the 530 * user name is actually split from the URI component. 531 * 532 * Access rights are checked against DokuWiki's ACL 533 * and applied accordingly. 534 * 535 * @param string $principalUri The principal URI to work on 536 * 537 * @return array An associative array of calendar IDs 538 */ 539 public function getCalendarIdsForUser($principalUri) 540 { 541 global $auth; 542 $user = explode('/', $principalUri); 543 $user = end($user); 544 $mapping = $this->getCalendarIdToPageMapping(); 545 $calids = array(); 546 $ud = $auth->getUserData($user); 547 $groups = $ud['grps']; 548 foreach($mapping as $row) 549 { 550 $id = $row['calid']; 551 $page = $row['page']; 552 $acl = auth_aclcheck($page, $user, $groups); 553 if($acl >= AUTH_READ) 554 { 555 $write = $acl > AUTH_READ; 556 $calids[$id] = array('readonly' => !$write); 557 } 558 } 559 return $calids; 560 } 561 562 /** 563 * Create a new calendar for a given page ID and set name and description 564 * accordingly. Also update the pagetocalendarmapping table on success. 565 * 566 * @param string $name The calendar's name 567 * @param string $description The calendar's description 568 * @param string $id (optional) The page ID to work on 569 * @param string $userid (optional) The user ID that created the calendar 570 * 571 * @return boolean True on success, otherwise false 572 */ 573 public function createCalendarForPage($name, $description, $id = null, $userid = null) 574 { 575 if(is_null($id)) 576 { 577 global $ID; 578 $id = $ID; 579 } 580 if(is_null($userid)) 581 { 582 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 583 { 584 $userid = $_SERVER['REMOTE_USER']; 585 } 586 else 587 { 588 $userid = uniqid('davcal-'); 589 } 590 } 591 $values = array('principals/'.$userid, 592 $name, 593 str_replace(array('/', ' ', ':'), '_', $id), 594 $description, 595 'VEVENT,VTODO', 596 0, 597 1); 598 $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ". 599 "VALUES (?, ?, ?, ?, ?, ?, ?)"; 600 $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]); 601 if($res === false) 602 return false; 603 604 // Get the new calendar ID 605 $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ". 606 "uri = ? AND description = ?"; 607 $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]); 608 $row = $this->sqlite->res2row($res); 609 610 // Update the pagetocalendarmapping table with the new calendar ID 611 if(isset($row['id'])) 612 { 613 $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)"; 614 $res = $this->sqlite->query($query, $id, $row['id']); 615 return ($res !== false); 616 } 617 618 return false; 619 } 620 621 /** 622 * Add a new calendar entry to the given calendar. Calendar data is 623 * specified as ICS file, thus it needs to be parsed first. 624 * 625 * This is mainly needed for the sync support. 626 * 627 * @param int $calid The calendar's ID 628 * @param string $uri The new object URI 629 * @param string $ics The ICS file 630 * 631 * @return mixed The etag. 632 */ 633 public function addCalendarEntryToCalendarByICS($calid, $uri, $ics) 634 { 635 $extraData = $this->getDenormalizedData($ics); 636 637 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)"; 638 $res = $this->sqlite->query($query, 639 $calid, 640 $uri, 641 $ics, 642 time(), 643 $extraData['etag'], 644 $extraData['size'], 645 $extraData['componentType'], 646 $extraData['firstOccurence'], 647 $extraData['lastOccurence'], 648 $extraData['uid']); 649 // If successfully, update the sync token database 650 if($res !== false) 651 { 652 $this->updateSyncTokenLog($calid, $uri, 'added'); 653 } 654 return $extraData['etag']; 655 } 656 657 /** 658 * Edit a calendar entry by providing a new ICS file. This is mainly 659 * needed for the sync support. 660 * 661 * @param int $calid The calendar's IS 662 * @param string $uri The object's URI to modify 663 * @param string $ics The new object's ICS file 664 */ 665 public function editCalendarEntryToCalendarByICS($calid, $uri, $ics) 666 { 667 $extraData = $this->getDenormalizedData($ics); 668 669 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?"; 670 $res = $this->sqlite->query($query, 671 $ics, 672 time(), 673 $extraData['etag'], 674 $extraData['size'], 675 $extraData['componentType'], 676 $extraData['firstOccurence'], 677 $extraData['lastOccurence'], 678 $extraData['uid'], 679 $calid, 680 $uri 681 ); 682 if($res !== false) 683 { 684 $this->updateSyncTokenLog($calid, $uri, 'modified'); 685 } 686 return $extraData['etag']; 687 } 688 689 /** 690 * Add a new iCal entry for a given page, i.e. a given calendar. 691 * 692 * The parameter array needs to contain 693 * detectedtz => The timezone as detected by the browser 694 * currenttz => The timezone in use by the calendar 695 * eventfrom => The event's start date 696 * eventfromtime => The event's start time 697 * eventto => The event's end date 698 * eventtotime => The event's end time 699 * eventname => The event's name 700 * eventdescription => The event's description 701 * 702 * @param string $id The page ID to work on 703 * @param string $user The user who created the calendar 704 * @param string $params A parameter array with values to create 705 * 706 * @return boolean True on success, otherwise false 707 */ 708 public function addCalendarEntryToCalendarForPage($id, $user, $params) 709 { 710 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 711 $timezone = new \DateTimeZone($params['currenttz']); 712 elseif($params['currenttz'] === 'local') 713 $timezone = new \DateTimeZone($params['detectedtz']); 714 else 715 $timezone = new \DateTimeZone('UTC'); 716 717 // Retrieve dates from settings 718 $startDate = explode('-', $params['eventfrom']); 719 $startTime = explode(':', $params['eventfromtime']); 720 $endDate = explode('-', $params['eventto']); 721 $endTime = explode(':', $params['eventtotime']); 722 723 // Load SabreDAV 724 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 725 $vcalendar = new \Sabre\VObject\Component\VCalendar(); 726 727 // Add VCalendar, UID and Event Name 728 $event = $vcalendar->add('VEVENT'); 729 $uuid = \Sabre\VObject\UUIDUtil::getUUID(); 730 $event->add('UID', $uuid); 731 $event->summary = $params['eventname']; 732 733 // Add a description if requested 734 $description = $params['eventdescription']; 735 if($description !== '') 736 $event->add('DESCRIPTION', $description); 737 738 // Add a location if requested 739 $location = $params['eventlocation']; 740 if($location !== '') 741 $event->add('LOCATION', $location); 742 743 // Add attachments 744 $attachments = $params['attachments']; 745 if(!is_null($attachments)) 746 foreach($attachments as $attachment) 747 $event->add('ATTACH', $attachment); 748 749 // Create a timestamp for last modified, created and dtstamp values in UTC 750 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 751 $event->add('DTSTAMP', $dtStamp); 752 $event->add('CREATED', $dtStamp); 753 $event->add('LAST-MODIFIED', $dtStamp); 754 755 // Adjust the start date, based on the given timezone information 756 $dtStart = new \DateTime(); 757 $dtStart->setTimezone($timezone); 758 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 759 760 // Only add the time values if it's not an allday event 761 if($params['allday'] != '1') 762 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 763 764 // Adjust the end date, based on the given timezone information 765 $dtEnd = new \DateTime(); 766 $dtEnd->setTimezone($timezone); 767 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 768 769 // Only add the time values if it's not an allday event 770 if($params['allday'] != '1') 771 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 772 773 // According to the VCal spec, we need to add a whole day here 774 if($params['allday'] == '1') 775 $dtEnd->add(new \DateInterval('P1D')); 776 777 // Really add Start and End events 778 $dtStartEv = $event->add('DTSTART', $dtStart); 779 $dtEndEv = $event->add('DTEND', $dtEnd); 780 781 // Adjust the DATE format for allday events 782 if($params['allday'] == '1') 783 { 784 $dtStartEv['VALUE'] = 'DATE'; 785 $dtEndEv['VALUE'] = 'DATE'; 786 } 787 788 $eventStr = $vcalendar->serialize(); 789 790 if(strpos($id, 'webdav://') === 0) 791 { 792 $wdc =& plugin_load('helper', 'webdavclient'); 793 if(is_null($wdc)) 794 return false; 795 $connectionId = str_replace('webdav://', '', $id); 796 return $wdc->addCalendarEntry($connectionId, $eventStr); 797 } 798 else 799 { 800 // Actually add the values to the database 801 $calid = $this->getCalendarIdForPage($id); 802 $uri = uniqid('dokuwiki-').'.ics'; 803 $now = new \DateTime(); 804 805 $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 806 $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT', 807 $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(), 808 strlen($eventStr), md5($eventStr), $uuid); 809 810 // If successfully, update the sync token database 811 if($res !== false) 812 { 813 $this->updateSyncTokenLog($calid, $uri, 'added'); 814 return true; 815 } 816 } 817 return false; 818 } 819 820 /** 821 * Retrieve the calendar settings of a given calendar id 822 * 823 * @param string $calid The calendar ID 824 * 825 * @return array The calendar settings array 826 */ 827 public function getCalendarSettings($calid) 828 { 829 $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? "; 830 $res = $this->sqlite->query($query, $calid); 831 $row = $this->sqlite->res2row($res); 832 return $row; 833 } 834 835 /** 836 * Retrieve all events that are within a given date range, 837 * based on the timezone setting. 838 * 839 * There is also support for retrieving recurring events, 840 * using Sabre's VObject Iterator. Recurring events are represented 841 * as individual calendar entries with the same UID. 842 * 843 * @param string $id The page ID to work with 844 * @param string $user The user ID to work with 845 * @param string $startDate The start date as a string 846 * @param string $endDate The end date as a string 847 * @param string $color (optional) The calendar's color 848 * 849 * @return array An array containing the calendar entries. 850 */ 851 public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null) 852 { 853 if($timezone !== '' && $timezone !== 'local') 854 $timezone = new \DateTimeZone($timezone); 855 else 856 $timezone = new \DateTimeZone('UTC'); 857 $data = array(); 858 859 $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?"; 860 $startTs = null; 861 $endTs = null; 862 if($startDate !== null) 863 { 864 $startTs = new \DateTime($startDate); 865 $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp()); 866 } 867 if($endDate !== null) 868 { 869 $endTs = new \DateTime($endDate); 870 $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp()); 871 } 872 873 // Load SabreDAV 874 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 875 876 if(strpos($id, 'webdav://') === 0) 877 { 878 $wdc =& plugin_load('helper', 'webdavclient'); 879 if(is_null($wdc)) 880 return $data; 881 $connectionId = str_replace('webdav://', '', $id); 882 $arr = $wdc->getCalendarEntries($connectionId, $startDate, $endDate); 883 } 884 else 885 { 886 $calid = $this->getCalendarIdForPage($id); 887 if(is_null($color)) 888 $color = $this->getCalendarColorForCalendar($calid); 889 890 // Retrieve matching calendar objects 891 $res = $this->sqlite->query($query, $calid); 892 $arr = $this->sqlite->res2arr($res); 893 } 894 895 // Parse individual calendar entries 896 foreach($arr as $row) 897 { 898 if(isset($row['calendardata'])) 899 { 900 $entry = array(); 901 $vcal = \Sabre\VObject\Reader::read($row['calendardata']); 902 $recurrence = $vcal->VEVENT->RRULE; 903 // If it is a recurring event, pass it through Sabre's EventIterator 904 if($recurrence != null) 905 { 906 $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); 907 $rEvents->rewind(); 908 while($rEvents->valid()) 909 { 910 $event = $rEvents->getEventObject(); 911 // If we are after the given time range, exit 912 if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp())) 913 break; 914 915 // If we are before the given time range, continue 916 if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())) 917 { 918 $rEvents->next(); 919 continue; 920 } 921 922 // If we are within the given time range, parse the event 923 $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); 924 $rEvents->next(); 925 } 926 } 927 else 928 $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); 929 } 930 } 931 return $data; 932 } 933 934 /** 935 * Helper function that parses the iCal data of a VEVENT to a calendar entry. 936 * 937 * @param \Sabre\VObject\VEvent $event The event to parse 938 * @param \DateTimeZone $timezone The timezone object 939 * @param string $uid The entry's UID 940 * @param boolean $recurring (optional) Set to true to define a recurring event 941 * 942 * @return array The parse calendar entry 943 */ 944 private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) 945 { 946 $entry = array(); 947 $start = $event->DTSTART; 948 // Parse only if the start date/time is present 949 if($start !== null) 950 { 951 $dtStart = $start->getDateTime(); 952 $dtStart->setTimezone($timezone); 953 954 // moment.js doesn't like times be given even if 955 // allDay is set to true 956 // This should fix T23 957 if($start['VALUE'] == 'DATE') 958 { 959 $entry['allDay'] = true; 960 $entry['start'] = $dtStart->format("Y-m-d"); 961 } 962 else 963 { 964 $entry['allDay'] = false; 965 $entry['start'] = $dtStart->format(\DateTime::ATOM); 966 } 967 } 968 $end = $event->DTEND; 969 // Parse only if the end date/time is present 970 if($end !== null) 971 { 972 $dtEnd = $end->getDateTime(); 973 $dtEnd->setTimezone($timezone); 974 if($end['VALUE'] == 'DATE') 975 $entry['end'] = $dtEnd->format("Y-m-d"); 976 else 977 $entry['end'] = $dtEnd->format(\DateTime::ATOM); 978 } 979 $description = $event->DESCRIPTION; 980 if($description !== null) 981 $entry['description'] = (string)$description; 982 else 983 $entry['description'] = ''; 984 $attachments = $event->ATTACH; 985 if($attachments !== null) 986 { 987 $entry['attachments'] = array(); 988 foreach($attachments as $attachment) 989 $entry['attachments'][] = (string)$attachment; 990 } 991 $entry['title'] = (string)$event->summary; 992 $entry['location'] = (string)$event->location; 993 $entry['id'] = $uid; 994 $entry['page'] = $page; 995 $entry['color'] = $color; 996 $entry['recurring'] = $recurring; 997 998 return $entry; 999 } 1000 1001 /** 1002 * Retrieve an event by its UID 1003 * 1004 * @param string $uid The event's UID 1005 * 1006 * @return mixed The table row with the given event 1007 */ 1008 public function getEventWithUid($uid) 1009 { 1010 $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?"; 1011 $res = $this->sqlite->query($query, $uid); 1012 $row = $this->sqlite->res2row($res); 1013 return $row; 1014 } 1015 1016 /** 1017 * Retrieve information of a calendar's object, not including the actual 1018 * calendar data! This is mainly neede for the sync support. 1019 * 1020 * @param int $calid The calendar ID 1021 * 1022 * @return mixed The result 1023 */ 1024 public function getCalendarObjects($calid) 1025 { 1026 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?"; 1027 $res = $this->sqlite->query($query, $calid); 1028 $arr = $this->sqlite->res2arr($res); 1029 return $arr; 1030 } 1031 1032 /** 1033 * Retrieve a single calendar object by calendar ID and URI 1034 * 1035 * @param int $calid The calendar's ID 1036 * @param string $uri The object's URI 1037 * 1038 * @return mixed The result 1039 */ 1040 public function getCalendarObjectByUri($calid, $uri) 1041 { 1042 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1043 $res = $this->sqlite->query($query, $calid, $uri); 1044 $row = $this->sqlite->res2row($res); 1045 return $row; 1046 } 1047 1048 /** 1049 * Retrieve several calendar objects by specifying an array of URIs. 1050 * This is mainly neede for sync. 1051 * 1052 * @param int $calid The calendar's ID 1053 * @param array $uris An array of URIs 1054 * 1055 * @return mixed The result 1056 */ 1057 public function getMultipleCalendarObjectsByUri($calid, $uris) 1058 { 1059 $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN ("; 1060 // Inserting a whole bunch of question marks 1061 $query .= implode(',', array_fill(0, count($uris), '?')); 1062 $query .= ')'; 1063 $vals = array_merge(array($calid), $uris); 1064 1065 $res = $this->sqlite->query($query, $vals); 1066 $arr = $this->sqlite->res2arr($res); 1067 return $arr; 1068 } 1069 1070 /** 1071 * Retrieve all calendar events for a given calendar ID 1072 * 1073 * @param string $calid The calendar's ID 1074 * 1075 * @return array An array containing all calendar data 1076 */ 1077 public function getAllCalendarEvents($calid) 1078 { 1079 $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?"; 1080 $res = $this->sqlite->query($query, $calid); 1081 $arr = $this->sqlite->res2arr($res); 1082 return $arr; 1083 } 1084 1085 /** 1086 * Edit a calendar entry for a page, given by its parameters. 1087 * The params array has the same format as @see addCalendarEntryForPage 1088 * 1089 * @param string $id The page's ID to work on 1090 * @param string $user The user's ID to work on 1091 * @param array $params The parameter array for the edited calendar event 1092 * 1093 * @return boolean True on success, otherwise false 1094 */ 1095 public function editCalendarEntryForPage($id, $user, $params) 1096 { 1097 if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') 1098 $timezone = new \DateTimeZone($params['currenttz']); 1099 elseif($params['currenttz'] === 'local') 1100 $timezone = new \DateTimeZone($params['detectedtz']); 1101 else 1102 $timezone = new \DateTimeZone('UTC'); 1103 1104 // Parse dates 1105 $startDate = explode('-', $params['eventfrom']); 1106 $startTime = explode(':', $params['eventfromtime']); 1107 $endDate = explode('-', $params['eventto']); 1108 $endTime = explode(':', $params['eventtotime']); 1109 1110 // Retrieve the existing event based on the UID 1111 $uid = $params['uid']; 1112 1113 if(strpos($id, 'webdav://') === 0) 1114 { 1115 $wdc =& plugin_load('helper', 'webdavclient'); 1116 if(is_null($wdc)) 1117 return false; 1118 $event = $wdc->getCalendarEntryByUid($uid); 1119 } 1120 else 1121 { 1122 $event = $this->getEventWithUid($uid); 1123 } 1124 1125 // Load SabreDAV 1126 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1127 if(!isset($event['calendardata'])) 1128 return false; 1129 $uri = $event['uri']; 1130 $calid = $event['calendarid']; 1131 1132 // Parse the existing event 1133 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1134 $vevent = $vcal->VEVENT; 1135 1136 // Set the new event values 1137 $vevent->summary = $params['eventname']; 1138 $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); 1139 $description = $params['eventdescription']; 1140 $location = $params['eventlocation']; 1141 1142 // Remove existing timestamps to overwrite them 1143 $vevent->remove('DESCRIPTION'); 1144 $vevent->remove('DTSTAMP'); 1145 $vevent->remove('LAST-MODIFIED'); 1146 $vevent->remove('ATTACH'); 1147 $vevent->remove('LOCATION'); 1148 1149 // Add new time stamps, description and location 1150 $vevent->add('DTSTAMP', $dtStamp); 1151 $vevent->add('LAST-MODIFIED', $dtStamp); 1152 if($description !== '') 1153 $vevent->add('DESCRIPTION', $description); 1154 if($location !== '') 1155 $vevent->add('LOCATION', $location); 1156 1157 // Add attachments 1158 $attachments = $params['attachments']; 1159 if(!is_null($attachments)) 1160 foreach($attachments as $attachment) 1161 $vevent->add('ATTACH', $attachment); 1162 1163 // Setup DTSTART 1164 $dtStart = new \DateTime(); 1165 $dtStart->setTimezone($timezone); 1166 $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); 1167 if($params['allday'] != '1') 1168 $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); 1169 1170 // Setup DTEND 1171 $dtEnd = new \DateTime(); 1172 $dtEnd->setTimezone($timezone); 1173 $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); 1174 if($params['allday'] != '1') 1175 $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); 1176 1177 // According to the VCal spec, we need to add a whole day here 1178 if($params['allday'] == '1') 1179 $dtEnd->add(new \DateInterval('P1D')); 1180 $vevent->remove('DTSTART'); 1181 $vevent->remove('DTEND'); 1182 $dtStartEv = $vevent->add('DTSTART', $dtStart); 1183 $dtEndEv = $vevent->add('DTEND', $dtEnd); 1184 1185 // Remove the time for allday events 1186 if($params['allday'] == '1') 1187 { 1188 $dtStartEv['VALUE'] = 'DATE'; 1189 $dtEndEv['VALUE'] = 'DATE'; 1190 } 1191 $eventStr = $vcal->serialize(); 1192 if(strpos($id, 'webdav://') === 0) 1193 { 1194 $connectionId = str_replace('webdav://', '', $id); 1195 return $wdc->editCalendarEntry($connectionId, $uid, $eventStr); 1196 } 1197 else 1198 { 1199 $now = new DateTime(); 1200 // Actually write to the database 1201 $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ". 1202 "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?"; 1203 $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(), 1204 $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid); 1205 if($res !== false) 1206 { 1207 $this->updateSyncTokenLog($calid, $uri, 'modified'); 1208 return true; 1209 } 1210 } 1211 return false; 1212 } 1213 1214 /** 1215 * Delete an event from a calendar by calendar ID and URI 1216 * 1217 * @param int $calid The calendar's ID 1218 * @param string $uri The object's URI 1219 * 1220 * @return true 1221 */ 1222 public function deleteCalendarEntryForCalendarByUri($calid, $uri) 1223 { 1224 $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1225 $res = $this->sqlite->query($query, $calid, $uri); 1226 if($res !== false) 1227 { 1228 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1229 } 1230 return true; 1231 } 1232 1233 /** 1234 * Delete a calendar entry for a given page. Actually, the event is removed 1235 * based on the entry's UID, so that page ID is no used. 1236 * 1237 * @param string $id The page's ID (unused) 1238 * @param array $params The parameter array to work with 1239 * 1240 * @return boolean True 1241 */ 1242 public function deleteCalendarEntryForPage($id, $params) 1243 { 1244 $uid = $params['uid']; 1245 if(strpos($id, 'webdav://') === 0) 1246 { 1247 $wdc =& plugin_load('helper', 'webdavclient'); 1248 if(is_null($wdc)) 1249 return false; 1250 $connectionId = str_replace('webdav://', '', $id); 1251 $result = $wdc->deleteCalendarEntry($connectionId, $uid); 1252 return $result; 1253 } 1254 $event = $this->getEventWithUid($uid); 1255 $calid = $event['calendarid']; 1256 $uri = $event['uri']; 1257 $query = "DELETE FROM calendarobjects WHERE uid = ?"; 1258 $res = $this->sqlite->query($query, $uid); 1259 if($res !== false) 1260 { 1261 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1262 } 1263 return true; 1264 } 1265 1266 /** 1267 * Retrieve the current sync token for a calendar 1268 * 1269 * @param string $calid The calendar id 1270 * 1271 * @return mixed The synctoken or false 1272 */ 1273 public function getSyncTokenForCalendar($calid) 1274 { 1275 $row = $this->getCalendarSettings($calid); 1276 if(isset($row['synctoken'])) 1277 return $row['synctoken']; 1278 return false; 1279 } 1280 1281 /** 1282 * Helper function to convert the operation name to 1283 * an operation code as stored in the database 1284 * 1285 * @param string $operationName The operation name 1286 * 1287 * @return mixed The operation code or false 1288 */ 1289 public function operationNameToOperation($operationName) 1290 { 1291 switch($operationName) 1292 { 1293 case 'added': 1294 return 1; 1295 break; 1296 case 'modified': 1297 return 2; 1298 break; 1299 case 'deleted': 1300 return 3; 1301 break; 1302 } 1303 return false; 1304 } 1305 1306 /** 1307 * Update the sync token log based on the calendar id and the 1308 * operation that was performed. 1309 * 1310 * @param string $calid The calendar ID that was modified 1311 * @param string $uri The calendar URI that was modified 1312 * @param string $operation The operation that was performed 1313 * 1314 * @return boolean True on success, otherwise false 1315 */ 1316 private function updateSyncTokenLog($calid, $uri, $operation) 1317 { 1318 $currentToken = $this->getSyncTokenForCalendar($calid); 1319 $operationCode = $this->operationNameToOperation($operation); 1320 if(($operationCode === false) || ($currentToken === false)) 1321 return false; 1322 $values = array($uri, 1323 $currentToken, 1324 $calid, 1325 $operationCode 1326 ); 1327 $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; 1328 $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode); 1329 if($res === false) 1330 return false; 1331 $currentToken++; 1332 $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; 1333 $res = $this->sqlite->query($query, $currentToken, $calid); 1334 return ($res !== false); 1335 } 1336 1337 /** 1338 * Return the sync URL for a given Page, i.e. a calendar 1339 * 1340 * @param string $id The page's ID 1341 * @param string $user (optional) The user's ID 1342 * 1343 * @return mixed The sync url or false 1344 */ 1345 public function getSyncUrlForPage($id, $user = null) 1346 { 1347 if(is_null($userid)) 1348 { 1349 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 1350 { 1351 $userid = $_SERVER['REMOTE_USER']; 1352 } 1353 else 1354 { 1355 return false; 1356 } 1357 } 1358 1359 $calid = $this->getCalendarIdForPage($id); 1360 if($calid === false) 1361 return false; 1362 1363 $calsettings = $this->getCalendarSettings($calid); 1364 if(!isset($calsettings['uri'])) 1365 return false; 1366 1367 $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; 1368 return $syncurl; 1369 } 1370 1371 /** 1372 * Return the private calendar's URL for a given page 1373 * 1374 * @param string $id the page ID 1375 * 1376 * @return mixed The private URL or false 1377 */ 1378 public function getPrivateURLForPage($id) 1379 { 1380 $calid = $this->getCalendarIdForPage($id); 1381 if($calid === false) 1382 return false; 1383 1384 return $this->getPrivateURLForCalendar($calid); 1385 } 1386 1387 /** 1388 * Return the private calendar's URL for a given calendar ID 1389 * 1390 * @param string $calid The calendar's ID 1391 * 1392 * @return mixed The private URL or false 1393 */ 1394 public function getPrivateURLForCalendar($calid) 1395 { 1396 if(isset($this->cachedValues['privateurl'][$calid])) 1397 return $this->cachedValues['privateurl'][$calid]; 1398 $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; 1399 $res = $this->sqlite->query($query, $calid); 1400 $row = $this->sqlite->res2row($res); 1401 if(!isset($row['url'])) 1402 { 1403 $url = uniqid("dokuwiki-").".ics"; 1404 $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; 1405 $res = $this->sqlite->query($query, $url, $calid); 1406 if($res === false) 1407 return false; 1408 } 1409 else 1410 { 1411 $url = $row['url']; 1412 } 1413 1414 $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; 1415 $this->cachedValues['privateurl'][$calid] = $url; 1416 return $url; 1417 } 1418 1419 /** 1420 * Retrieve the calendar ID for a given private calendar URL 1421 * 1422 * @param string $url The private URL 1423 * 1424 * @return mixed The calendar ID or false 1425 */ 1426 public function getCalendarForPrivateURL($url) 1427 { 1428 $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; 1429 $res = $this->sqlite->query($query, $url); 1430 $row = $this->sqlite->res2row($res); 1431 if(!isset($row['calid'])) 1432 return false; 1433 return $row['calid']; 1434 } 1435 1436 /** 1437 * Return a given calendar as ICS feed, i.e. all events in one ICS file. 1438 * 1439 * @param string $calid The calendar ID to retrieve 1440 * 1441 * @return mixed The calendar events as string or false 1442 */ 1443 public function getCalendarAsICSFeed($calid) 1444 { 1445 $calSettings = $this->getCalendarSettings($calid); 1446 if($calSettings === false) 1447 return false; 1448 $events = $this->getAllCalendarEvents($calid); 1449 if($events === false) 1450 return false; 1451 1452 // Load SabreDAV 1453 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1454 $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; 1455 $out .= $calSettings['displayname']."\r\n"; 1456 foreach($events as $event) 1457 { 1458 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1459 $evt = $vcal->VEVENT; 1460 $out .= $evt->serialize(); 1461 } 1462 $out .= "END:VCALENDAR\r\n"; 1463 return $out; 1464 } 1465 1466 /** 1467 * Retrieve a configuration option for the plugin 1468 * 1469 * @param string $key The key to query 1470 * @return mixed The option set, null if not found 1471 */ 1472 public function getConfig($key) 1473 { 1474 return $this->getConf($key); 1475 } 1476 1477 /** 1478 * Parses some information from calendar objects, used for optimized 1479 * calendar-queries. Taken nearly unmodified from Sabre's PDO backend 1480 * 1481 * Returns an array with the following keys: 1482 * * etag - An md5 checksum of the object without the quotes. 1483 * * size - Size of the object in bytes 1484 * * componentType - VEVENT, VTODO or VJOURNAL 1485 * * firstOccurence 1486 * * lastOccurence 1487 * * uid - value of the UID property 1488 * 1489 * @param string $calendarData 1490 * @return array 1491 */ 1492 protected function getDenormalizedData($calendarData) 1493 { 1494 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1495 1496 $vObject = \Sabre\VObject\Reader::read($calendarData); 1497 $componentType = null; 1498 $component = null; 1499 $firstOccurence = null; 1500 $lastOccurence = null; 1501 $uid = null; 1502 foreach ($vObject->getComponents() as $component) 1503 { 1504 if ($component->name !== 'VTIMEZONE') 1505 { 1506 $componentType = $component->name; 1507 $uid = (string)$component->UID; 1508 break; 1509 } 1510 } 1511 if (!$componentType) 1512 { 1513 return false; 1514 } 1515 if ($componentType === 'VEVENT') 1516 { 1517 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1518 // Finding the last occurence is a bit harder 1519 if (!isset($component->RRULE)) 1520 { 1521 if (isset($component->DTEND)) 1522 { 1523 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1524 } 1525 elseif (isset($component->DURATION)) 1526 { 1527 $endDate = clone $component->DTSTART->getDateTime(); 1528 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1529 $lastOccurence = $endDate->getTimeStamp(); 1530 } 1531 elseif (!$component->DTSTART->hasTime()) 1532 { 1533 $endDate = clone $component->DTSTART->getDateTime(); 1534 $endDate->modify('+1 day'); 1535 $lastOccurence = $endDate->getTimeStamp(); 1536 } 1537 else 1538 { 1539 $lastOccurence = $firstOccurence; 1540 } 1541 } 1542 else 1543 { 1544 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1545 $maxDate = new \DateTime('2038-01-01'); 1546 if ($it->isInfinite()) 1547 { 1548 $lastOccurence = $maxDate->getTimeStamp(); 1549 } 1550 else 1551 { 1552 $end = $it->getDtEnd(); 1553 while ($it->valid() && $end < $maxDate) 1554 { 1555 $end = $it->getDtEnd(); 1556 $it->next(); 1557 } 1558 $lastOccurence = $end->getTimeStamp(); 1559 } 1560 } 1561 } 1562 1563 return array( 1564 'etag' => md5($calendarData), 1565 'size' => strlen($calendarData), 1566 'componentType' => $componentType, 1567 'firstOccurence' => $firstOccurence, 1568 'lastOccurence' => $lastOccurence, 1569 'uid' => $uid, 1570 ); 1571 1572 } 1573 1574 /** 1575 * Query a calendar by ID and taking several filters into account. 1576 * This is heavily based on Sabre's PDO backend. 1577 * 1578 * @param int $calendarId The calendar's ID 1579 * @param array $filters The filter array to apply 1580 * 1581 * @return mixed The result 1582 */ 1583 public function calendarQuery($calendarId, $filters) 1584 { 1585 dbglog('davcal::helper::calendarQuery'); 1586 $componentType = null; 1587 $requirePostFilter = true; 1588 $timeRange = null; 1589 1590 // if no filters were specified, we don't need to filter after a query 1591 if (!$filters['prop-filters'] && !$filters['comp-filters']) 1592 { 1593 $requirePostFilter = false; 1594 } 1595 1596 // Figuring out if there's a component filter 1597 if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) 1598 { 1599 $componentType = $filters['comp-filters'][0]['name']; 1600 1601 // Checking if we need post-filters 1602 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) 1603 { 1604 $requirePostFilter = false; 1605 } 1606 // There was a time-range filter 1607 if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) 1608 { 1609 $timeRange = $filters['comp-filters'][0]['time-range']; 1610 1611 // If start time OR the end time is not specified, we can do a 1612 // 100% accurate mysql query. 1613 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) 1614 { 1615 $requirePostFilter = false; 1616 } 1617 } 1618 1619 } 1620 1621 if ($requirePostFilter) 1622 { 1623 $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; 1624 } 1625 else 1626 { 1627 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1628 } 1629 1630 $values = array( 1631 $calendarId 1632 ); 1633 1634 if ($componentType) 1635 { 1636 $query .= " AND componenttype = ?"; 1637 $values[] = $componentType; 1638 } 1639 1640 if ($timeRange && $timeRange['start']) 1641 { 1642 $query .= " AND lastoccurence > ?"; 1643 $values[] = $timeRange['start']->getTimeStamp(); 1644 } 1645 if ($timeRange && $timeRange['end']) 1646 { 1647 $query .= " AND firstoccurence < ?"; 1648 $values[] = $timeRange['end']->getTimeStamp(); 1649 } 1650 1651 dbglog($query); 1652 $res = $this->sqlite->query($query, $values); 1653 $arr = $this->sqlite->res2arr($res); 1654 dbglog($arr); 1655 1656 $result = array(); 1657 foreach($arr as $row) 1658 { 1659 if ($requirePostFilter) 1660 { 1661 dbglog('requirePostFilter for'); 1662 dbglog($row); 1663 dbglog($filters); 1664 if (!$this->validateFilterForObject($row, $filters)) 1665 { 1666 continue; 1667 } 1668 } 1669 $result[] = $row['uri']; 1670 1671 } 1672 1673 return $result; 1674 } 1675 1676 /** 1677 * This method validates if a filter (as passed to calendarQuery) matches 1678 * the given object. Taken from Sabre's PDO backend 1679 * 1680 * @param array $object 1681 * @param array $filters 1682 * @return bool 1683 */ 1684 protected function validateFilterForObject($object, $filters) 1685 { 1686 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1687 // Unfortunately, setting the 'calendardata' here is optional. If 1688 // it was excluded, we actually need another call to get this as 1689 // well. 1690 if (!isset($object['calendardata'])) 1691 { 1692 dbglog('fetching object...'); 1693 $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); 1694 } 1695 1696 dbglog('object to validate: '); 1697 dbglog($object); 1698 $vObject = \Sabre\VObject\Reader::read($object['calendardata']); 1699 $validator = new \Sabre\CalDAV\CalendarQueryValidator(); 1700 1701 $res = $validator->validate($vObject, $filters); 1702 dbglog($res); 1703 return $res; 1704 1705 } 1706 1707 /** 1708 * Retrieve changes for a given calendar based on the given syncToken. 1709 * 1710 * @param int $calid The calendar's ID 1711 * @param int $syncToken The supplied sync token 1712 * @param int $syncLevel The sync level 1713 * @param int $limit The limit of changes 1714 * 1715 * @return array The result 1716 */ 1717 public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) 1718 { 1719 // Current synctoken 1720 $currentToken = $this->getSyncTokenForCalendar($calid); 1721 1722 if ($currentToken === false) return null; 1723 1724 $result = array( 1725 'syncToken' => $currentToken, 1726 'added' => array(), 1727 'modified' => array(), 1728 'deleted' => array(), 1729 ); 1730 1731 if ($syncToken) 1732 { 1733 1734 $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; 1735 if ($limit > 0) $query .= " LIMIT " . (int)$limit; 1736 1737 // Fetching all changes 1738 $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid); 1739 if($res === false) 1740 return null; 1741 1742 $arr = $this->sqlite->res2arr($res); 1743 $changes = array(); 1744 1745 // This loop ensures that any duplicates are overwritten, only the 1746 // last change on a node is relevant. 1747 foreach($arr as $row) 1748 { 1749 $changes[$row['uri']] = $row['operation']; 1750 } 1751 1752 foreach ($changes as $uri => $operation) 1753 { 1754 switch ($operation) 1755 { 1756 case 1 : 1757 $result['added'][] = $uri; 1758 break; 1759 case 2 : 1760 $result['modified'][] = $uri; 1761 break; 1762 case 3 : 1763 $result['deleted'][] = $uri; 1764 break; 1765 } 1766 1767 } 1768 } 1769 else 1770 { 1771 // No synctoken supplied, this is the initial sync. 1772 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1773 $res = $this->sqlite->query($query); 1774 $arr = $this->sqlite->res2arr($res); 1775 1776 $result['added'] = $arr; 1777 } 1778 return $result; 1779 } 1780 1781} 1782