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