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