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 an event from a calendar by calendar ID and URI 1195 * 1196 * @param int $calid The calendar's ID 1197 * @param string $uri The object's URI 1198 * 1199 * @return true 1200 */ 1201 public function deleteCalendarEntryForCalendarByUri($calid, $uri) 1202 { 1203 $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; 1204 $res = $this->sqlite->query($query, $calid, $uri); 1205 if($res !== false) 1206 { 1207 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1208 } 1209 return true; 1210 } 1211 1212 /** 1213 * Delete a calendar entry for a given page. Actually, the event is removed 1214 * based on the entry's UID, so that page ID is no used. 1215 * 1216 * @param string $id The page's ID (unused) 1217 * @param array $params The parameter array to work with 1218 * 1219 * @return boolean True 1220 */ 1221 public function deleteCalendarEntryForPage($id, $params) 1222 { 1223 $uid = $params['uid']; 1224 if(strpos($id, 'webdav://') === 0) 1225 { 1226 $wdc =& plugin_load('helper', 'webdavclient'); 1227 if(is_null($wdc)) 1228 return false; 1229 $connectionId = str_replace('webdav://', '', $id); 1230 $result = $wdc->deleteCalendarEntry($connectionId, $uid); 1231 return $result; 1232 } 1233 $event = $this->getEventWithUid($uid); 1234 $calid = $event['calendarid']; 1235 $uri = $event['uri']; 1236 $query = "DELETE FROM calendarobjects WHERE uid = ?"; 1237 $res = $this->sqlite->query($query, $uid); 1238 if($res !== false) 1239 { 1240 $this->updateSyncTokenLog($calid, $uri, 'deleted'); 1241 } 1242 return true; 1243 } 1244 1245 /** 1246 * Retrieve the current sync token for a calendar 1247 * 1248 * @param string $calid The calendar id 1249 * 1250 * @return mixed The synctoken or false 1251 */ 1252 public function getSyncTokenForCalendar($calid) 1253 { 1254 $row = $this->getCalendarSettings($calid); 1255 if(isset($row['synctoken'])) 1256 return $row['synctoken']; 1257 return false; 1258 } 1259 1260 /** 1261 * Helper function to convert the operation name to 1262 * an operation code as stored in the database 1263 * 1264 * @param string $operationName The operation name 1265 * 1266 * @return mixed The operation code or false 1267 */ 1268 public function operationNameToOperation($operationName) 1269 { 1270 switch($operationName) 1271 { 1272 case 'added': 1273 return 1; 1274 break; 1275 case 'modified': 1276 return 2; 1277 break; 1278 case 'deleted': 1279 return 3; 1280 break; 1281 } 1282 return false; 1283 } 1284 1285 /** 1286 * Update the sync token log based on the calendar id and the 1287 * operation that was performed. 1288 * 1289 * @param string $calid The calendar ID that was modified 1290 * @param string $uri The calendar URI that was modified 1291 * @param string $operation The operation that was performed 1292 * 1293 * @return boolean True on success, otherwise false 1294 */ 1295 private function updateSyncTokenLog($calid, $uri, $operation) 1296 { 1297 $currentToken = $this->getSyncTokenForCalendar($calid); 1298 $operationCode = $this->operationNameToOperation($operation); 1299 if(($operationCode === false) || ($currentToken === false)) 1300 return false; 1301 $values = array($uri, 1302 $currentToken, 1303 $calid, 1304 $operationCode 1305 ); 1306 $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; 1307 $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode); 1308 if($res === false) 1309 return false; 1310 $currentToken++; 1311 $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; 1312 $res = $this->sqlite->query($query, $currentToken, $calid); 1313 return ($res !== false); 1314 } 1315 1316 /** 1317 * Return the sync URL for a given Page, i.e. a calendar 1318 * 1319 * @param string $id The page's ID 1320 * @param string $user (optional) The user's ID 1321 * 1322 * @return mixed The sync url or false 1323 */ 1324 public function getSyncUrlForPage($id, $user = null) 1325 { 1326 if(is_null($userid)) 1327 { 1328 if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) 1329 { 1330 $userid = $_SERVER['REMOTE_USER']; 1331 } 1332 else 1333 { 1334 return false; 1335 } 1336 } 1337 1338 $calid = $this->getCalendarIdForPage($id); 1339 if($calid === false) 1340 return false; 1341 1342 $calsettings = $this->getCalendarSettings($calid); 1343 if(!isset($calsettings['uri'])) 1344 return false; 1345 1346 $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; 1347 return $syncurl; 1348 } 1349 1350 /** 1351 * Return the private calendar's URL for a given page 1352 * 1353 * @param string $id the page ID 1354 * 1355 * @return mixed The private URL or false 1356 */ 1357 public function getPrivateURLForPage($id) 1358 { 1359 $calid = $this->getCalendarIdForPage($id); 1360 if($calid === false) 1361 return false; 1362 1363 return $this->getPrivateURLForCalendar($calid); 1364 } 1365 1366 /** 1367 * Return the private calendar's URL for a given calendar ID 1368 * 1369 * @param string $calid The calendar's ID 1370 * 1371 * @return mixed The private URL or false 1372 */ 1373 public function getPrivateURLForCalendar($calid) 1374 { 1375 if(isset($this->cachedValues['privateurl'][$calid])) 1376 return $this->cachedValues['privateurl'][$calid]; 1377 $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; 1378 $res = $this->sqlite->query($query, $calid); 1379 $row = $this->sqlite->res2row($res); 1380 if(!isset($row['url'])) 1381 { 1382 $url = uniqid("dokuwiki-").".ics"; 1383 $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; 1384 $res = $this->sqlite->query($query, $url, $calid); 1385 if($res === false) 1386 return false; 1387 } 1388 else 1389 { 1390 $url = $row['url']; 1391 } 1392 1393 $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; 1394 $this->cachedValues['privateurl'][$calid] = $url; 1395 return $url; 1396 } 1397 1398 /** 1399 * Retrieve the calendar ID for a given private calendar URL 1400 * 1401 * @param string $url The private URL 1402 * 1403 * @return mixed The calendar ID or false 1404 */ 1405 public function getCalendarForPrivateURL($url) 1406 { 1407 $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; 1408 $res = $this->sqlite->query($query, $url); 1409 $row = $this->sqlite->res2row($res); 1410 if(!isset($row['calid'])) 1411 return false; 1412 return $row['calid']; 1413 } 1414 1415 /** 1416 * Return a given calendar as ICS feed, i.e. all events in one ICS file. 1417 * 1418 * @param string $calid The calendar ID to retrieve 1419 * 1420 * @return mixed The calendar events as string or false 1421 */ 1422 public function getCalendarAsICSFeed($calid) 1423 { 1424 $calSettings = $this->getCalendarSettings($calid); 1425 if($calSettings === false) 1426 return false; 1427 $events = $this->getAllCalendarEvents($calid); 1428 if($events === false) 1429 return false; 1430 1431 // Load SabreDAV 1432 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1433 $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; 1434 $out .= $calSettings['displayname']."\r\n"; 1435 foreach($events as $event) 1436 { 1437 $vcal = \Sabre\VObject\Reader::read($event['calendardata']); 1438 $evt = $vcal->VEVENT; 1439 $out .= $evt->serialize(); 1440 } 1441 $out .= "END:VCALENDAR\r\n"; 1442 return $out; 1443 } 1444 1445 /** 1446 * Retrieve a configuration option for the plugin 1447 * 1448 * @param string $key The key to query 1449 * @return mixed The option set, null if not found 1450 */ 1451 public function getConfig($key) 1452 { 1453 return $this->getConf($key); 1454 } 1455 1456 /** 1457 * Parses some information from calendar objects, used for optimized 1458 * calendar-queries. Taken nearly unmodified from Sabre's PDO backend 1459 * 1460 * Returns an array with the following keys: 1461 * * etag - An md5 checksum of the object without the quotes. 1462 * * size - Size of the object in bytes 1463 * * componentType - VEVENT, VTODO or VJOURNAL 1464 * * firstOccurence 1465 * * lastOccurence 1466 * * uid - value of the UID property 1467 * 1468 * @param string $calendarData 1469 * @return array 1470 */ 1471 protected function getDenormalizedData($calendarData) 1472 { 1473 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1474 1475 $vObject = \Sabre\VObject\Reader::read($calendarData); 1476 $componentType = null; 1477 $component = null; 1478 $firstOccurence = null; 1479 $lastOccurence = null; 1480 $uid = null; 1481 foreach ($vObject->getComponents() as $component) 1482 { 1483 if ($component->name !== 'VTIMEZONE') 1484 { 1485 $componentType = $component->name; 1486 $uid = (string)$component->UID; 1487 break; 1488 } 1489 } 1490 if (!$componentType) 1491 { 1492 return false; 1493 } 1494 if ($componentType === 'VEVENT') 1495 { 1496 $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); 1497 // Finding the last occurence is a bit harder 1498 if (!isset($component->RRULE)) 1499 { 1500 if (isset($component->DTEND)) 1501 { 1502 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); 1503 } 1504 elseif (isset($component->DURATION)) 1505 { 1506 $endDate = clone $component->DTSTART->getDateTime(); 1507 $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); 1508 $lastOccurence = $endDate->getTimeStamp(); 1509 } 1510 elseif (!$component->DTSTART->hasTime()) 1511 { 1512 $endDate = clone $component->DTSTART->getDateTime(); 1513 $endDate->modify('+1 day'); 1514 $lastOccurence = $endDate->getTimeStamp(); 1515 } 1516 else 1517 { 1518 $lastOccurence = $firstOccurence; 1519 } 1520 } 1521 else 1522 { 1523 $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); 1524 $maxDate = new \DateTime('2038-01-01'); 1525 if ($it->isInfinite()) 1526 { 1527 $lastOccurence = $maxDate->getTimeStamp(); 1528 } 1529 else 1530 { 1531 $end = $it->getDtEnd(); 1532 while ($it->valid() && $end < $maxDate) 1533 { 1534 $end = $it->getDtEnd(); 1535 $it->next(); 1536 } 1537 $lastOccurence = $end->getTimeStamp(); 1538 } 1539 } 1540 } 1541 1542 return array( 1543 'etag' => md5($calendarData), 1544 'size' => strlen($calendarData), 1545 'componentType' => $componentType, 1546 'firstOccurence' => $firstOccurence, 1547 'lastOccurence' => $lastOccurence, 1548 'uid' => $uid, 1549 ); 1550 1551 } 1552 1553 /** 1554 * Query a calendar by ID and taking several filters into account. 1555 * This is heavily based on Sabre's PDO backend. 1556 * 1557 * @param int $calendarId The calendar's ID 1558 * @param array $filters The filter array to apply 1559 * 1560 * @return mixed The result 1561 */ 1562 public function calendarQuery($calendarId, $filters) 1563 { 1564 $componentType = null; 1565 $requirePostFilter = true; 1566 $timeRange = null; 1567 1568 // if no filters were specified, we don't need to filter after a query 1569 if (!$filters['prop-filters'] && !$filters['comp-filters']) 1570 { 1571 $requirePostFilter = false; 1572 } 1573 1574 // Figuring out if there's a component filter 1575 if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) 1576 { 1577 $componentType = $filters['comp-filters'][0]['name']; 1578 1579 // Checking if we need post-filters 1580 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) 1581 { 1582 $requirePostFilter = false; 1583 } 1584 // There was a time-range filter 1585 if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) 1586 { 1587 $timeRange = $filters['comp-filters'][0]['time-range']; 1588 1589 // If start time OR the end time is not specified, we can do a 1590 // 100% accurate mysql query. 1591 if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) 1592 { 1593 $requirePostFilter = false; 1594 } 1595 } 1596 1597 } 1598 1599 if ($requirePostFilter) 1600 { 1601 $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; 1602 } 1603 else 1604 { 1605 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1606 } 1607 1608 $values = array( 1609 $calendarId 1610 ); 1611 1612 if ($componentType) 1613 { 1614 $query .= " AND componenttype = ?"; 1615 $values[] = $componentType; 1616 } 1617 1618 if ($timeRange && $timeRange['start']) 1619 { 1620 $query .= " AND lastoccurence > ?"; 1621 $values[] = $timeRange['start']->getTimeStamp(); 1622 } 1623 if ($timeRange && $timeRange['end']) 1624 { 1625 $query .= " AND firstoccurence < ?"; 1626 $values[] = $timeRange['end']->getTimeStamp(); 1627 } 1628 1629 $res = $this->sqlite->query($query, $values); 1630 $arr = $this->sqlite->res2arr($res); 1631 1632 $result = array(); 1633 foreach($arr as $row) 1634 { 1635 if ($requirePostFilter) 1636 { 1637 if (!$this->validateFilterForObject($row, $filters)) 1638 { 1639 continue; 1640 } 1641 } 1642 $result[] = $row['uri']; 1643 1644 } 1645 1646 return $result; 1647 } 1648 1649 /** 1650 * This method validates if a filter (as passed to calendarQuery) matches 1651 * the given object. Taken from Sabre's PDO backend 1652 * 1653 * @param array $object 1654 * @param array $filters 1655 * @return bool 1656 */ 1657 protected function validateFilterForObject($object, $filters) 1658 { 1659 require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); 1660 // Unfortunately, setting the 'calendardata' here is optional. If 1661 // it was excluded, we actually need another call to get this as 1662 // well. 1663 if (!isset($object['calendardata'])) 1664 { 1665 $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); 1666 } 1667 1668 $vObject = \Sabre\VObject\Reader::read($object['calendardata']); 1669 $validator = new \Sabre\CalDAV\CalendarQueryValidator(); 1670 1671 return $validator->validate($vObject, $filters); 1672 1673 } 1674 1675 /** 1676 * Retrieve changes for a given calendar based on the given syncToken. 1677 * 1678 * @param int $calid The calendar's ID 1679 * @param int $syncToken The supplied sync token 1680 * @param int $syncLevel The sync level 1681 * @param int $limit The limit of changes 1682 * 1683 * @return array The result 1684 */ 1685 public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) 1686 { 1687 // Current synctoken 1688 $currentToken = $this->getSyncTokenForCalendar($calid); 1689 1690 if ($currentToken === false) return null; 1691 1692 $result = array( 1693 'syncToken' => $currentToken, 1694 'added' => array(), 1695 'modified' => array(), 1696 'deleted' => array(), 1697 ); 1698 1699 if ($syncToken) 1700 { 1701 1702 $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; 1703 if ($limit > 0) $query .= " LIMIT " . (int)$limit; 1704 1705 // Fetching all changes 1706 $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid); 1707 if($res === false) 1708 return null; 1709 1710 $arr = $this->sqlite->res2arr($res); 1711 $changes = array(); 1712 1713 // This loop ensures that any duplicates are overwritten, only the 1714 // last change on a node is relevant. 1715 foreach($arr as $row) 1716 { 1717 $changes[$row['uri']] = $row['operation']; 1718 } 1719 1720 foreach ($changes as $uri => $operation) 1721 { 1722 switch ($operation) 1723 { 1724 case 1 : 1725 $result['added'][] = $uri; 1726 break; 1727 case 2 : 1728 $result['modified'][] = $uri; 1729 break; 1730 case 3 : 1731 $result['deleted'][] = $uri; 1732 break; 1733 } 1734 1735 } 1736 } 1737 else 1738 { 1739 // No synctoken supplied, this is the initial sync. 1740 $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; 1741 $res = $this->sqlite->query($query); 1742 $arr = $this->sqlite->res2arr($res); 1743 1744 $result['added'] = $arr; 1745 } 1746 return $result; 1747 } 1748 1749} 1750