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