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